use crate::{cli, config::Config, db, resume, search, session::Session, tui};
use std::path::Path;
pub mod doctor;
pub mod skill;
pub fn run(cli: cli::Cli, cfg: Config) -> anyhow::Result<()> {
let options = db::Options {
include_children: cli.include_children,
};
if let Some(query) = cli.search.as_deref() {
return search_query(&cfg, options, query, cfg.limit, false);
}
match cli.command {
None => open(&cfg, options, ""),
Some(cli::Command::Search(args)) => search_cmd(&cfg, options, args),
Some(cli::Command::Recent(args)) => recent(&cfg, options, args.limit, args.json),
Some(cli::Command::Last(args)) => last(&cfg, options, args.print),
Some(cli::Command::Resume(args)) => resume_cmd(&cfg, options, &args.id),
Some(cli::Command::Print(args)) => print_cmd(&cfg, options, &args.id),
Some(cli::Command::Copy(args)) => copy_cmd(&cfg, options, &args.id),
Some(cli::Command::Scan(args)) => scan(&args.include),
Some(cli::Command::Doctor) => doctor::run(&cfg, options),
Some(cli::Command::Skill(args)) => skill::run(args),
}
}
fn open(cfg: &Config, options: db::Options, query: &str) -> anyhow::Result<()> {
let paths = db::discover(cfg);
let list = db::load(&paths, cfg.limit, options);
if let Some(session) = tui::pick(cfg, &list, query)? {
resume::run(cfg, &session)?;
}
Ok(())
}
fn search_cmd(cfg: &Config, options: db::Options, args: cli::SearchArgs) -> anyhow::Result<()> {
let query = args.query.join(" ");
search_query(cfg, options, &query, args.limit, args.no_tui)
}
fn search_query(
cfg: &Config,
options: db::Options,
query: &str,
limit: usize,
no_tui: bool,
) -> anyhow::Result<()> {
let paths = db::discover(cfg);
let list = db::load(&paths, cfg.limit.max(limit), options);
let matches = search::filter(&list, query, limit);
if no_tui {
print_list(cfg, &matches);
return Ok(());
}
if let Some(session) = tui::pick(cfg, &matches, query)? {
resume::run(cfg, &session)?;
}
Ok(())
}
fn recent(cfg: &Config, options: db::Options, limit: usize, json: bool) -> anyhow::Result<()> {
let list = db::load(&db::discover(cfg), limit, options);
if json {
println!("{}", serde_json::to_string_pretty(&list)?);
} else {
print_list(cfg, &list);
}
Ok(())
}
fn last(cfg: &Config, options: db::Options, print: bool) -> anyhow::Result<()> {
let Some(session) = db::load(&db::discover(cfg), 1, options).into_iter().next() else {
anyhow::bail!("no sessions found");
};
if print {
println!("{}", resume::command(cfg, &session));
return Ok(());
}
resume::run(cfg, &session)
}
fn resume_cmd(cfg: &Config, options: db::Options, id: &str) -> anyhow::Result<()> {
resume::run(cfg, &db::load_one(&db::discover(cfg), id, options)?)
}
fn print_cmd(cfg: &Config, options: db::Options, id: &str) -> anyhow::Result<()> {
println!(
"{}",
resume::command(cfg, &db::load_one(&db::discover(cfg), id, options)?)
);
Ok(())
}
fn copy_cmd(cfg: &Config, options: db::Options, id: &str) -> anyhow::Result<()> {
let session = db::load_one(&db::discover(cfg), id, options)?;
tui::copy_text(&resume::command(cfg, &session))?;
println!("copied resume command for {}", session.id);
Ok(())
}
fn scan(include: &[std::path::PathBuf]) -> anyhow::Result<()> {
for path in db::scan(include) {
println!("{}", path.display());
}
Ok(())
}
fn print_list(cfg: &Config, list: &[Session]) {
if list.is_empty() {
println!("No sessions found.");
return;
}
for (index, session) in list.iter().enumerate() {
if index > 0 {
println!();
}
println!(
"{} {}",
session.title,
format_time(session.updated).unwrap_or_else(|| "unknown".to_string())
);
println!(" id: {}", session.id);
println!(" path: {}", pretty_path(&session.directory));
if let Some(agent) = session.agent.as_deref().filter(|agent| !agent.is_empty()) {
println!(" agent: {agent}");
}
println!(" command: {}", resume::command(cfg, session));
}
}
fn format_time(ms: i64) -> Option<String> {
let ms = u64::try_from(ms).ok()?;
let time = std::time::UNIX_EPOCH.checked_add(std::time::Duration::from_millis(ms))?;
Some(
chrono::DateTime::<chrono::Local>::from(time)
.format("%Y-%m-%d %I:%M %p")
.to_string(),
)
}
fn pretty_path(path: &Path) -> String {
let Some(home) = dirs::home_dir() else {
return path.display().to_string();
};
let Ok(rest) = path.strip_prefix(home) else {
return path.display().to_string();
};
if rest.as_os_str().is_empty() {
return "~".to_string();
}
format!("~/{}", rest.display())
}