mod app;
mod campaign;
mod connector;
mod event;
mod json_highlight;
mod storage;
mod ui;
use anyhow::Result;
use clap::{Parser, Subcommand};
use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{backend::CrosstermBackend, Terminal};
use std::io;
use app::App;
use event::{Event, EventHandler};
#[derive(Parser)]
#[command(name = "terapi", version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
#[arg(long, value_name = "FILE", global = true)]
demo: Option<String>,
}
#[derive(Subcommand)]
enum Commands {
Run {
#[arg(value_name = "CAMPAIGN")]
file: String,
#[arg(long, short = 's')]
silent: bool,
},
Import {
#[arg(value_name = "FILE")]
file: String,
},
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Some(Commands::Run { file, silent }) => {
let camp = campaign::load(&file)?;
campaign::run(&camp, silent).await?;
}
Some(Commands::Import { file }) => {
import_collection(&file)?;
}
None => launch_tui(load_json(cli.demo.as_deref())).await?,
}
Ok(())
}
fn import_collection(path: &str) -> Result<()> {
use anyhow::Context;
let content = std::fs::read_to_string(path)
.with_context(|| format!("cannot read '{}'", path))?;
let col: storage::StoredCollection = toml::from_str(&content)
.with_context(|| format!("'{}' is not a valid collection TOML", path))?;
let dir = storage::resolve_terapi_dir().join("collections");
std::fs::create_dir_all(&dir)?;
let filename = storage::sanitize_filename(&col.collection.name);
let dest = dir.join(format!("{}.toml", filename));
let existed = dest.exists();
std::fs::write(&dest, &content)?;
if existed {
println!("Updated \"{}\" → {}", col.collection.name, dest.display());
} else {
println!("Imported \"{}\" → {}", col.collection.name, dest.display());
}
Ok(())
}
fn load_json(path: Option<&str>) -> Option<String> {
let p = path?;
match std::fs::read_to_string(p) {
Ok(content) => Some(content),
Err(e) => {
eprintln!("terapi: cannot read '{}': {}", p, e);
None
}
}
}
async fn launch_tui(json: Option<String>) -> Result<()> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(io::stdout());
let mut terminal = Terminal::new(backend)?;
let result = run_tui(&mut terminal, json).await;
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
result
}
async fn run_tui(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>, json: Option<String>) -> Result<()> {
let mut app = App::new(json);
let events = EventHandler::new(250);
while app.running {
terminal.draw(|frame| ui::render(frame, &app))?;
match events.next()? {
Event::Key(key) => app.handle_key(key)?,
Event::Tick => app.tick(),
}
}
Ok(())
}