219 lines
7.0 KiB
Rust
219 lines
7.0 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 = 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<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);
|
|
#[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<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(())
|
|
}
|
|
|
|
/// 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<Option<crossterm::event::Event>> {
|
|
if crossterm::event::poll(std::time::Duration::from_millis(100))? {
|
|
Ok(Some(crossterm::event::read()?))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|