Files
ostiary/src/main.rs
2026-05-22 00:24:19 +07:00

153 lines
4.6 KiB
Rust

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 = config::Config::load()?;
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());
app.load_menu().await;
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<CrosstermBackend<io::Stdout>>,
app: &mut app::App,
event_rx: &mut mpsc::UnboundedReceiver<app::Event>,
) -> 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);
// fallback for non-unix or if exec failed
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<CrosstermBackend<io::Stdout>>,
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(())
}
async fn read_crossterm_event() -> Result<Option<crossterm::event::Event>> {
if crossterm::event::poll(std::time::Duration::from_millis(100))? {
Ok(Some(crossterm::event::read()?))
} else {
Ok(None)
}
}