mod api;
mod app;
mod cli;
mod config;
mod models;
mod ui;
use anyhow::Result;
use app::App;
use cli::Args;
use config::Config;
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{backend::CrosstermBackend, Terminal};
use std::io;
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse_args();
let config = if let Some(ref path) = args.config {
Config::load(path)?
} else {
Config::load_or_default()
};
if args.init {
let path = Config::default_config_path()
.ok_or_else(|| anyhow::anyhow!("Could not determine config directory"))?;
if path.exists() && !args.force {
eprintln!("Config file already exists: {}", path.display());
eprintln!("Use --force to overwrite.");
std::process::exit(1);
}
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&path, config::sample_config())?;
println!("Config file written to: {}", path.display());
std::process::exit(0);
}
let mut app = App::new(&args, &config)?;
if app.symbols.is_empty() {
eprintln!("Error: No symbols to watch.");
eprintln!("Provide symbols via -s flag or config file.");
eprintln!();
eprintln!("Example: stonktop -s AAPL,GOOGL,BTC-USD");
eprintln!();
eprintln!(
"Or create a config file at {:?}",
Config::default_config_path()
);
eprintln!();
eprintln!("Sample config:");
eprintln!("{}", config::sample_config());
std::process::exit(1);
}
if app.batch_mode {
run_batch(&mut app, &args.format).await
} else {
run_interactive(&mut app).await
}
}
async fn run_batch(app: &mut App, format: &cli::OutputFormat) -> Result<()> {
loop {
app.refresh().await?;
ui::render_batch(app, format);
if app.should_quit() {
break;
}
tokio::time::sleep(app.refresh_interval).await;
}
Ok(())
}
async fn run_interactive(app: &mut App) -> Result<()> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
app.refresh().await?;
let result = run_app(&mut terminal, app).await;
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
result
}
async fn run_app(
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
app: &mut App,
) -> Result<()> {
let tick_rate = Duration::from_millis(100);
loop {
terminal.draw(|f| ui::render(f, app))?;
if crossterm::event::poll(tick_rate)? {
if let Event::Key(key) = event::read()? {
if app.secure_mode {
match key.code {
KeyCode::Char('q') | KeyCode::Esc => app.quit(),
KeyCode::Up | KeyCode::Char('k') => app.select_up(),
KeyCode::Down | KeyCode::Char('j') => app.select_down(),
_ => {}
}
} else {
app.handle_key_event(key.code, key.modifiers);
}
}
}
if app.should_quit() {
break;
}
if app.needs_refresh() {
app.refresh().await?;
}
}
Ok(())
}