use clap::Parser;
use cli::args::Args;
use color_eyre::Result;
use event::{Event, EventHandler};
use vortix::app::App;
use vortix::{cli, config, constants, event, ui};
fn main() -> Result<()> {
color_eyre::install()?;
let default_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
restore_terminal();
eprintln!();
eprintln!("Vortix crashed unexpectedly.");
eprintln!("If your network is broken, run: vortix release-killswitch");
eprintln!();
default_hook(info);
}));
let args = Args::parse();
let config_dir_source = if args.config_dir.is_some() {
if std::env::var("VORTIX_CONFIG_DIR").is_ok() {
let env_val = std::env::var("VORTIX_CONFIG_DIR").unwrap_or_default();
let cli_val = args
.config_dir
.as_ref()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default();
if cli_val == env_val {
"from VORTIX_CONFIG_DIR"
} else {
"from --config-dir"
}
} else {
"from --config-dir"
}
} else {
"default"
};
let explicit_override = args.config_dir.is_some();
let mut config_dir = config::resolve_config_dir(args.config_dir.as_ref())
.map_err(|e| color_eyre::eyre::eyre!("Failed to resolve config directory: {e}"))?;
if !explicit_override {
if let Some(old_dir) = config::check_migration(&config_dir) {
config_dir = prompt_migration(&old_dir, &config_dir);
}
}
config::set_config_dir(config_dir.clone());
let app_config = match config::load_config(&config_dir) {
Ok(cfg) => cfg,
Err(e) => {
eprintln!("Error: {e}");
eprintln!();
eprintln!("Fix the file or remove it to use defaults:");
eprintln!(" nano {}/config.toml", config_dir.display());
eprintln!(" rm {}/config.toml", config_dir.display());
std::process::exit(1);
}
};
let output_mode = if args.json {
cli::output::OutputMode::Json
} else if args.quiet {
cli::output::OutputMode::Quiet
} else {
cli::output::OutputMode::Human
};
if let Some(command) = &args.command {
let exit_code = cli::commands::handle_command(
command,
&config_dir,
config_dir_source,
&app_config,
output_mode,
);
std::process::exit(exit_code);
}
let terminal = init_terminal()?;
let result = run_tui(terminal, app_config, config_dir);
restore_terminal();
result
}
fn prompt_migration(old_dir: &std::path::Path, new_dir: &std::path::Path) -> std::path::PathBuf {
use std::io::Write;
eprintln!();
eprintln!(" Old data found at: {}", old_dir.display());
eprintln!(" New config dir: {}", new_dir.display());
eprintln!();
eprintln!(" Vortix now stores config under your home directory instead of");
eprintln!(" /root, so profiles are accessible without sudo.");
eprintln!();
eprintln!(" [Y] Move your existing profiles and settings to the new location.");
eprintln!(" Files are copied first, then deleted from the old path.");
eprintln!();
eprintln!(
" [n] Start fresh. Your old data stays at {} but",
old_dir.display()
);
eprintln!(" won't be used. You can import profiles again or copy manually.");
eprintln!();
eprint!(" Move data? [Y/n] ");
let _ = std::io::stderr().flush();
let mut input = String::new();
if std::io::stdin().read_line(&mut input).is_err() {
eprintln!(" Could not read input. Starting fresh.\n");
return new_dir.to_path_buf();
}
let input = input.trim().to_lowercase();
if input.is_empty() || input == "y" || input == "yes" {
eprintln!();
match config::migrate_data(old_dir, new_dir) {
Ok(()) => {
let profiles_exist = new_dir.join("profiles").is_dir()
&& std::fs::read_dir(new_dir.join("profiles"))
.map(|mut d| d.next().is_some())
.unwrap_or(false);
if profiles_exist {
eprintln!(" Done! Data moved to {}\n", new_dir.display());
} else {
eprintln!(
" Warning: Move completed but no profiles found at {}",
new_dir.join("profiles").display()
);
eprintln!(
" Check if your profiles are still at {}\n",
old_dir.display()
);
}
new_dir.to_path_buf()
}
Err(e) => {
eprintln!(" Move failed: {e}");
eprintln!(" Your original data is untouched at {}", old_dir.display());
eprintln!(" Starting fresh at {}\n", new_dir.display());
new_dir.to_path_buf()
}
}
} else {
eprintln!();
eprintln!(" Starting fresh at {}", new_dir.display());
eprintln!(" Old data is still at {}.", old_dir.display());
eprintln!(" This prompt will appear until you migrate or the old data is removed.");
eprintln!(" To silence it: --config-dir {}\n", old_dir.display());
new_dir.to_path_buf()
}
}
fn run_tui(
mut terminal: ratatui::DefaultTerminal,
config: config::AppConfig,
config_dir: std::path::PathBuf,
) -> Result<()> {
let tick_rate = config.tick_rate;
let mut app = App::new(config, config_dir);
let events = EventHandler::new(tick_rate);
let size = terminal.size()?;
app.on_resize(size.width, size.height);
app.process_external();
terminal.draw(|frame| ui::render(frame, &mut app))?;
while !app.should_quit {
if app.has_active_animation() {
while let Some(event) = events.try_next()? {
match event {
Event::Key(key_event) => app.handle_key(key_event),
Event::Mouse(mouse_event) => app.handle_mouse(mouse_event),
Event::Tick => app.on_tick(),
Event::Resize(w, h) => app.on_resize(w, h),
}
}
app.advance_animation();
} else {
match events.next()? {
Event::Key(key_event) => app.handle_key(key_event),
Event::Mouse(mouse_event) => app.handle_mouse(mouse_event),
Event::Tick => app.on_tick(),
Event::Resize(width, height) => app.on_resize(width, height),
}
}
app.process_external();
terminal.draw(|frame| ui::render(frame, &mut app))?;
if app.has_active_animation() {
std::thread::sleep(std::time::Duration::from_millis(
constants::FLIP_ANIMATION_FRAME_MS,
));
}
}
Ok(())
}
fn init_terminal() -> Result<ratatui::DefaultTerminal> {
let mut terminal = ratatui::init();
crossterm::execute!(std::io::stdout(), crossterm::event::EnableMouseCapture)?;
terminal.clear()?;
Ok(terminal)
}
fn restore_terminal() {
let _ = crossterm::execute!(std::io::stdout(), crossterm::event::DisableMouseCapture);
ratatui::restore();
}