mod ansi; mod app; mod config; mod error; mod executor; mod menu; mod network; mod ui; mod updater; use std::io; use anyhow::Result; use crossterm::{ event::{DisableMouseCapture, EnableMouseCapture}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use ratatui::{backend::CrosstermBackend, Terminal}; use tokio::sync::mpsc; #[tokio::main] async fn main() -> Result<()> { let config = if config::Config::exists() { config::Config::load()? } else { let (url, name) = prompt_first_endpoint()?; config::Config::create_with_endpoint(&url, &name)? }; enable_raw_mode()?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; let (event_tx, mut event_rx) = mpsc::unbounded_channel(); let mut app = app::App::new(config, event_tx.clone()); // Detect failed update: if the previous run wrote an expected version but // the actual version didn't change, the replacement didn't work. // Show an error and skip the update check to break any infinite loop. let update_ok = match updater::take_update_target() { Some(expected) if expected != env!("CARGO_PKG_VERSION") => { app.error = Some(format!( "Обновление не применилось: ожидалась v{expected}, \ запущена v{}. Проверьте, что в релизе загружен правильный бинарник.", env!("CARGO_PKG_VERSION") )); false } _ => true, }; app.load_menu().await; if update_ok { app.check_update(); } let res = run_app(&mut terminal, &mut app, &mut event_rx).await; disable_raw_mode()?; execute!( terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture )?; terminal.show_cursor()?; if let Err(err) = res { println!("Ошибка: {:?}", err); } Ok(()) } async fn run_app( terminal: &mut Terminal>, app: &mut app::App, event_rx: &mut mpsc::UnboundedReceiver, ) -> Result<()> { loop { terminal.draw(|f| ui::render(f, app))?; let event = tokio::select! { crossterm_event = read_crossterm_event() => { match crossterm_event { Ok(Some(evt)) => app::Event::Crossterm(evt), Ok(None) => continue, Err(e) => app::Event::Error(e.to_string()), } } Some(evt) = event_rx.recv() => evt, }; if app.handle_event(event).await? { break; } // Check for pending restart (after update applied) — before pending_exec. if let Some(exe_path) = app.pending_restart.take() { disable_raw_mode()?; execute!( terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture )?; terminal.show_cursor()?; #[cfg(unix)] updater::exec_updated(&exe_path); #[cfg(not(unix))] break; } // After handling events, check if a script requested an exec. if let Some(exec) = app.pending_exec.take() { run_exec(terminal, exec).await?; } } Ok(()) } /// Suspends ratatui, hands the terminal to an external interactive process, /// then restores the TUI and sends the exit code back to the script. async fn run_exec( terminal: &mut Terminal>, exec: app::PendingExec, ) -> Result<()> { // ── Suspend ratatui ────────────────────────────────────────────────────── disable_raw_mode()?; execute!( terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture )?; terminal.show_cursor()?; // ── Run the program with inherited stdin/stdout/stderr ─────────────────── let exit_code = tokio::task::spawn_blocking({ let shell = exec.shell.clone(); move || { std::process::Command::new("bash") .arg("-c") .arg(&shell) .stdin(std::process::Stdio::inherit()) .stdout(std::process::Stdio::inherit()) .stderr(std::process::Stdio::inherit()) .status() .map(|s| s.code().unwrap_or(1)) .unwrap_or(1) } }) .await?; // ── Restore ratatui ────────────────────────────────────────────────────── enable_raw_mode()?; execute!( terminal.backend_mut(), EnterAlternateScreen, EnableMouseCapture )?; terminal.clear()?; // ── Reply to the script with the exit code ─────────────────────────────── let _ = exec.reply_tx.send(exit_code.to_string()); Ok(()) } /// Runs before ratatui starts. Prompts for endpoint URL and name on first launch. fn prompt_first_endpoint() -> Result<(String, String)> { use std::io::Write; println!(); println!(" ╔══════════════════════════════════╗"); println!(" ║ Ostiary — первый запуск ║"); println!(" ╚══════════════════════════════════╝"); println!(); println!(" Конфиг будет сохранён в:"); println!(" {}", config::config_path().display()); println!(); let name = loop { print!(" Название эндпоинта: "); std::io::stdout().flush()?; let mut buf = String::new(); std::io::stdin().read_line(&mut buf)?; let val = buf.trim().to_string(); if val.is_empty() { println!(" Название не может быть пустым. Попробуйте ещё раз.\n"); continue; } break val; }; let url = loop { print!(" URL эндпоинта: "); std::io::stdout().flush()?; let mut buf = String::new(); std::io::stdin().read_line(&mut buf)?; let val = buf.trim().to_string(); if val.is_empty() { println!(" URL не может быть пустым. Попробуйте ещё раз.\n"); continue; } break val; }; println!(); Ok((url, name)) } async fn read_crossterm_event() -> Result> { if crossterm::event::poll(std::time::Duration::from_millis(100))? { Ok(Some(crossterm::event::read()?)) } else { Ok(None) } }