use std::fmt::Write as _;
use std::net::SocketAddr;
use std::path::PathBuf;
use clap::builder::styling::{AnsiColor, Effects, Style, Styles};
use clap::{Args, CommandFactory, FromArgMatches, Parser, Subcommand};
const HEADING_STYLE: Style = AnsiColor::Blue.on_default().effects(Effects::BOLD);
const LITERAL_STYLE: Style = AnsiColor::Magenta.on_default();
const PLACEHOLDER_STYLE: Style = Style::new();
const DESCRIPTION_STYLE: Style = Style::new();
const STYLES: Styles = Styles::styled()
.header(HEADING_STYLE)
.usage(HEADING_STYLE)
.literal(LITERAL_STYLE)
.placeholder(PLACEHOLDER_STYLE)
.context(DESCRIPTION_STYLE)
.context_value(AnsiColor::Yellow.on_default())
.valid(AnsiColor::Green.on_default())
.invalid(AnsiColor::Red.on_default().effects(Effects::BOLD))
.error(AnsiColor::Red.on_default().effects(Effects::BOLD));
const HELP_SECTIONS: &[HelpSection] = &[
HelpSection {
heading: "TASKS",
commands: &[
"add",
"list",
"search",
"context",
"show",
"update",
"note",
"note-delete",
"dep",
"epic",
"text",
"bulk-update",
"delete",
"restore",
],
},
HelpSection {
heading: "WORKSPACE",
commands: &["workspace", "project", "label"],
},
HelpSection {
heading: "SYNC",
commands: &["sync", "server", "conflict", "daemon"],
},
HelpSection {
heading: "INTERACTIVE",
commands: &["tui"],
},
HelpSection {
heading: "AGENTS",
commands: &["prime", "skill"],
},
HelpSection {
heading: "SETUP",
commands: &["config", "doctor"],
},
HelpSection {
heading: "DATA SAFETY",
commands: &["backup", "export", "import"],
},
];
struct HelpSection {
heading: &'static str,
commands: &'static [&'static str],
}
pub(crate) fn parse() -> Cli {
let mut command = Cli::command();
let help = render_top_level_help(&command);
command = command.override_help(help);
let matches = command.get_matches();
Cli::from_arg_matches(&matches).expect("clap validates matches")
}
fn render_top_level_help(command: &clap::Command) -> String {
let mut help = String::new();
writeln!(&mut help, "Local-first task manager").unwrap();
writeln!(&mut help).unwrap();
writeln!(
&mut help,
"{} aven {} {}",
paint("USAGE:", HEADING_STYLE),
paint("[OPTIONS]", LITERAL_STYLE),
paint("<COMMAND>", PLACEHOLDER_STYLE)
)
.unwrap();
writeln!(&mut help).unwrap();
for section in HELP_SECTIONS {
render_section(&mut help, command, section);
}
render_help_section(&mut help);
render_options_section(&mut help);
help
}
fn render_section(help: &mut String, command: &clap::Command, section: &HelpSection) {
writeln!(help, "{}", paint(section.heading, HEADING_STYLE)).unwrap();
for name in section.commands {
let about = command_about(command, name).unwrap_or_default();
render_row(help, name, &paint(name, LITERAL_STYLE), &about, 13);
}
writeln!(help).unwrap();
}
fn render_help_section(help: &mut String) {
writeln!(help, "{}", paint("HELP", HEADING_STYLE)).unwrap();
render_row(
help,
"help",
&paint("help", LITERAL_STYLE),
"Print this message or the help of the given subcommand(s)",
13,
);
writeln!(help).unwrap();
}
fn render_options_section(help: &mut String) {
writeln!(help, "{}", paint("OPTIONS", HEADING_STYLE)).unwrap();
render_row(
help,
"--db <DB>",
&format!(
"{} {}",
paint("--db", LITERAL_STYLE),
paint("<DB>", PLACEHOLDER_STYLE)
),
"Use a specific SQLite database path",
27,
);
render_row(
help,
"--workspace <WORKSPACE>",
&format!(
"{} {}",
paint("--workspace", LITERAL_STYLE),
paint("<WORKSPACE>", PLACEHOLDER_STYLE)
),
"Use a specific workspace by name or key",
27,
);
render_row(
help,
"-V, --version",
&format!(
"{}, {}",
paint("-V", LITERAL_STYLE),
paint("--version", LITERAL_STYLE)
),
"Print version",
27,
);
render_row(
help,
"-h, --help",
&format!(
"{}, {}",
paint("-h", LITERAL_STYLE),
paint("--help", LITERAL_STYLE)
),
"Print help",
27,
);
}
fn command_about(command: &clap::Command, name: &str) -> Option<String> {
command
.get_subcommands()
.find(|subcommand| subcommand.get_name() == name)
.and_then(|subcommand| subcommand.get_about())
.map(|about| about.to_string())
}
fn render_row(
help: &mut String,
plain_name: &str,
styled_name: &str,
description: &str,
width: usize,
) {
write!(help, " {styled_name}").unwrap();
for _ in plain_name.len()..width {
help.push(' ');
}
writeln!(help, "{}", paint(description, DESCRIPTION_STYLE)).unwrap();
}
fn paint(text: &str, style: Style) -> String {
format!("{}{}{}", style.render(), text, style.render_reset())
}
#[derive(Parser)]
#[command(name = "aven")]
#[command(about = "Local-first task manager")]
#[command(version)]
#[command(styles = STYLES)]
pub struct Cli {
#[arg(long, global = true, help = "Use a specific SQLite database path")]
pub(crate) db: Option<PathBuf>,
#[arg(long, global = true, help = "Use a specific workspace by name or key")]
pub(crate) workspace: Option<String>,
#[command(subcommand)]
pub(crate) command: Commands,
}
#[derive(Subcommand)]
pub(crate) enum Commands {
Add(AddArgs),
Dep(DepCommand),
Epic(EpicCommand),
Context(ContextArgs),
Show(ShowArgs),
List(ListArgs),
Search(TaskSearchArgs),
BulkUpdate(BulkUpdateArgs),
Prime(PrimeArgs),
Update(UpdateArgs),
Note(NoteArgs),
NoteDelete(NoteDeleteArgs),
Delete(RefArgs),
Restore(RefArgs),
Text(TextCommand),
Label(LabelCommand),
Project(ProjectCommand),
Workspace(WorkspaceCommand),
Conflict(ConflictCommand),
Config(ConfigCommand),
Backup(BackupCommand),
Export(ExportArgs),
Import(ImportArgs),
Skill(SkillCommand),
Doctor(DoctorArgs),
Daemon(DaemonArgs),
Server(ServerArgs),
Sync(SyncArgs),
Tui(TuiArgs),
#[command(hide = true)]
Internal(InternalCommand),
}
#[derive(Args)]
pub(crate) struct SkillCommand {
#[command(subcommand)]
pub(crate) command: Option<SkillSubcommand>,
}
#[derive(Subcommand)]
pub(crate) enum SkillSubcommand {
Install(SkillInstallArgs),
}
#[derive(Args)]
pub(crate) struct SkillInstallArgs {
#[arg(long = "agent", value_enum)]
pub(crate) agent: Vec<CodingAgentArg>,
}
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum CodingAgentArg {
Claude,
Opencode,
Codex,
}
#[derive(Args)]
pub(crate) struct TuiArgs {
#[arg(short = 'p', long, num_args = 0..=1, default_missing_value = "")]
pub(crate) project: Option<String>,
#[arg(long)]
pub(crate) add_task: bool,
#[arg(long)]
pub(crate) add_task_only: bool,
#[arg(long)]
pub(crate) natural: bool,
}
#[derive(Args)]
pub(crate) struct InternalCommand {
#[command(subcommand)]
pub(crate) command: InternalSubcommand,
}
#[derive(Subcommand)]
pub(crate) enum InternalSubcommand {
#[command(name = "natural-add", hide = true)]
NaturalAdd(InternalNaturalAddArgs),
}
#[derive(Args)]
pub(crate) struct InternalNaturalAddArgs {
#[arg(long)]
pub(crate) workspace_id: String,
#[arg(long)]
pub(crate) project: Option<String>,
#[arg(long, allow_hyphen_values = true)]
pub(crate) input: String,
#[arg(long, hide = true)]
pub(crate) tui_undo: bool,
}
#[derive(Args)]
pub(crate) struct AddArgs {
pub(crate) title: String,
#[arg(long)]
pub(crate) project: Option<String>,
#[arg(long)]
pub(crate) description: Option<String>,
#[arg(long)]
pub(crate) description_file: Option<PathBuf>,
#[arg(long)]
pub(crate) description_stdin: bool,
#[arg(long, default_value = "none")]
pub(crate) priority: String,
#[arg(long)]
pub(crate) label: Vec<String>,
#[arg(long, help = "Create the task as an epic container")]
pub(crate) epic: bool,
#[arg(long)]
pub(crate) natural: bool,
}
#[derive(Args)]
pub(crate) struct ShowArgs {
pub(crate) task_ref: String,
#[arg(long)]
pub(crate) full: bool,
#[arg(long, help = "Print machine-readable JSON")]
pub(crate) json: bool,
}
#[derive(Args)]
pub(crate) struct ContextArgs {
pub(crate) task_ref: String,
#[arg(long, help = "Print machine-readable JSON")]
pub(crate) json: bool,
}
#[derive(Args)]
pub(crate) struct ListArgs {
#[arg(long)]
pub(crate) project: Option<String>,
#[arg(long)]
pub(crate) status: Option<String>,
#[arg(long)]
pub(crate) priority: Option<String>,
#[arg(long)]
pub(crate) label: Option<String>,
#[arg(long)]
pub(crate) all: bool,
#[arg(long)]
pub(crate) deleted: bool,
#[arg(long)]
pub(crate) ready: bool,
#[arg(long)]
pub(crate) blocked: bool,
#[arg(long)]
pub(crate) epics: bool,
#[arg(long)]
pub(crate) limit: Option<usize>,
#[arg(long, help = "Print machine-readable JSON")]
pub(crate) json: bool,
}
#[derive(Args)]
pub(crate) struct TaskSearchArgs {
pub(crate) query: Vec<String>,
#[arg(long, default_value_t = 50)]
pub(crate) limit: usize,
#[arg(long, help = "Include deleted tasks")]
pub(crate) all: bool,
#[arg(long, help = "Print machine-readable JSON")]
pub(crate) json: bool,
}
#[derive(Args)]
pub(crate) struct DepCommand {
#[command(subcommand)]
pub(crate) command: DepSubcommand,
}
#[derive(Subcommand)]
pub(crate) enum DepSubcommand {
Add(DepAddArgs),
Remove(DepRemoveArgs),
List(DepListArgs),
}
#[derive(Args)]
pub(crate) struct DepAddArgs {
pub(crate) task_ref: String,
pub(crate) depends_on_ref: String,
}
#[derive(Args)]
pub(crate) struct DepRemoveArgs {
pub(crate) task_ref: String,
pub(crate) depends_on_ref: String,
}
#[derive(Args)]
pub(crate) struct DepListArgs {
pub(crate) task_ref: String,
#[arg(long, help = "Print machine-readable JSON")]
pub(crate) json: bool,
}
#[derive(Args)]
pub(crate) struct EpicCommand {
#[command(subcommand)]
pub(crate) command: EpicSubcommand,
}
#[derive(Subcommand)]
pub(crate) enum EpicSubcommand {
Add(EpicAddArgs),
Remove(EpicRemoveArgs),
List(EpicListArgs),
}
#[derive(Args)]
pub(crate) struct EpicAddArgs {
pub(crate) child_ref: String,
pub(crate) epic_ref: String,
}
#[derive(Args)]
pub(crate) struct EpicRemoveArgs {
pub(crate) child_ref: String,
pub(crate) epic_ref: String,
}
#[derive(Args)]
pub(crate) struct EpicListArgs {
pub(crate) epic_ref: String,
#[arg(long, help = "Print machine-readable JSON")]
pub(crate) json: bool,
}
#[derive(Args)]
pub(crate) struct BulkUpdateArgs {
#[arg(long)]
pub(crate) project: Option<String>,
#[arg(long)]
pub(crate) status: Option<String>,
#[arg(long)]
pub(crate) priority: Option<String>,
#[arg(long)]
pub(crate) filter_label: Option<String>,
#[arg(long)]
pub(crate) all: bool,
#[arg(long)]
pub(crate) include_deleted: bool,
#[arg(long)]
pub(crate) dry_run: bool,
#[arg(long)]
pub(crate) set_status: Option<String>,
#[arg(long)]
pub(crate) set_priority: Option<String>,
#[arg(long)]
pub(crate) set_project: Option<String>,
#[arg(long)]
pub(crate) label: Vec<String>,
#[arg(long)]
pub(crate) remove_label: Vec<String>,
}
#[derive(Args)]
pub(crate) struct PrimeArgs {
#[arg(long)]
pub(crate) project: Option<String>,
#[arg(long)]
pub(crate) limit: Option<usize>,
#[arg(long, help = "Print machine-readable JSON")]
pub(crate) json: bool,
}
#[derive(Args)]
pub(crate) struct UpdateArgs {
pub(crate) task_ref: String,
#[arg(long)]
pub(crate) title: Option<String>,
#[arg(long)]
pub(crate) description: Option<String>,
#[arg(long)]
pub(crate) description_file: Option<PathBuf>,
#[arg(long)]
pub(crate) description_stdin: bool,
#[arg(long)]
pub(crate) project: Option<String>,
#[arg(long)]
pub(crate) status: Option<String>,
#[arg(long)]
pub(crate) priority: Option<String>,
#[arg(long, value_name = "on|off")]
pub(crate) epic: Option<String>,
#[arg(long)]
pub(crate) label: Vec<String>,
#[arg(long)]
pub(crate) remove_label: Vec<String>,
}
#[derive(Args)]
pub(crate) struct NoteArgs {
pub(crate) task_ref: String,
pub(crate) text: Option<String>,
#[arg(long)]
pub(crate) file: Option<PathBuf>,
#[arg(long)]
pub(crate) stdin: bool,
}
#[derive(Args)]
pub(crate) struct NoteDeleteArgs {
pub(crate) task_ref: String,
pub(crate) note_id: String,
}
#[derive(Args)]
pub(crate) struct SearchArgs {
#[arg(long)]
pub(crate) search: Option<String>,
#[arg(long)]
pub(crate) limit: Option<usize>,
#[arg(long, help = "Print machine-readable JSON")]
pub(crate) json: bool,
}
#[derive(Args)]
pub(crate) struct RefArgs {
pub(crate) task_ref: String,
}
#[derive(Args)]
pub(crate) struct LabelCommand {
#[command(subcommand)]
pub(crate) command: LabelSubcommand,
}
#[derive(Subcommand)]
pub(crate) enum LabelSubcommand {
Create {
name: String,
},
Delete {
name: String,
},
List(SearchArgs),
}
#[derive(Args)]
pub(crate) struct ProjectCommand {
#[command(subcommand)]
pub(crate) command: ProjectSubcommand,
}
#[derive(Subcommand)]
pub(crate) enum ProjectSubcommand {
Create {
name: String,
#[arg(long)]
path: Option<PathBuf>,
},
Delete { project: String },
List(SearchArgs),
Rename {
project: String,
new_name: String,
#[arg(long)]
prefix: Option<String>,
},
Path {
#[command(subcommand)]
command: ProjectPathSubcommand,
},
}
#[derive(Subcommand)]
pub(crate) enum ProjectPathSubcommand {
Add { project: String, path: PathBuf },
Remove { project: String, path: PathBuf },
List { project: Option<String> },
}
#[derive(Args)]
pub(crate) struct WorkspaceCommand {
#[command(subcommand)]
pub(crate) command: WorkspaceSubcommand,
}
#[derive(Args)]
pub(crate) struct BackupCommand {
#[command(subcommand)]
pub(crate) command: Option<BackupSubcommand>,
#[arg(long)]
pub(crate) output: Option<PathBuf>,
}
#[derive(Subcommand)]
pub(crate) enum BackupSubcommand {
Restore(BackupRestoreArgs),
}
#[derive(Args)]
pub(crate) struct BackupRestoreArgs {
pub(crate) path: PathBuf,
#[arg(long)]
pub(crate) yes: bool,
}
#[derive(Args)]
pub(crate) struct ExportArgs {
#[arg(long)]
pub(crate) output: PathBuf,
}
#[derive(Args)]
pub(crate) struct ImportArgs {
pub(crate) path: PathBuf,
#[arg(long)]
pub(crate) yes: bool,
}
#[derive(Args)]
pub(crate) struct DoctorArgs {
#[arg(long)]
pub(crate) integrity: bool,
#[arg(long, help = "Print machine-readable JSON")]
pub(crate) json: bool,
}
#[derive(Subcommand)]
pub(crate) enum WorkspaceSubcommand {
List,
Create { name: String },
Rename { workspace: String, new_name: String },
}
#[derive(Args)]
pub(crate) struct ConflictCommand {
#[command(subcommand)]
pub(crate) command: ConflictSubcommand,
}
#[derive(Subcommand)]
pub(crate) enum ConflictSubcommand {
List {
#[arg(long)]
project: Option<String>,
#[arg(long)]
field: Option<String>,
#[arg(long)]
limit: Option<usize>,
#[arg(long, help = "Print machine-readable JSON")]
json: bool,
},
Diff {
task_ref: String,
field: String,
},
Export {
task_ref: String,
field: String,
#[arg(long)]
dir: PathBuf,
},
Show {
task_ref: String,
#[arg(long)]
field: Option<String>,
#[arg(long, help = "Print machine-readable JSON")]
json: bool,
},
Resolve {
task_ref: String,
field: String,
#[arg(long)]
#[arg(long = "use")]
use_variant: Option<String>,
#[arg(long)]
value: Option<String>,
#[arg(long)]
value_file: Option<PathBuf>,
#[arg(long)]
value_stdin: bool,
},
}
#[derive(Args)]
pub(crate) struct ConfigCommand {
#[command(subcommand)]
pub(crate) command: ConfigSubcommand,
}
#[derive(Args)]
pub(crate) struct TextCommand {
#[command(subcommand)]
pub(crate) command: TextSubcommand,
}
#[derive(Subcommand)]
pub(crate) enum TextSubcommand {
Get(TextGetArgs),
Diff(TextDiffArgs),
Set(TextSetArgs),
}
#[derive(Args)]
pub(crate) struct TextGetArgs {
pub(crate) task_ref: String,
pub(crate) field: String,
#[arg(long)]
pub(crate) raw: bool,
#[arg(long)]
pub(crate) output: Option<PathBuf>,
}
#[derive(Args)]
pub(crate) struct TextDiffArgs {
pub(crate) task_ref: String,
pub(crate) field: String,
#[arg(long)]
pub(crate) file: PathBuf,
}
#[derive(Args)]
pub(crate) struct TextSetArgs {
pub(crate) task_ref: String,
pub(crate) field: String,
#[arg(long)]
pub(crate) file: Option<PathBuf>,
#[arg(long)]
pub(crate) stdin: bool,
#[arg(long)]
pub(crate) if_sha256: String,
}
#[derive(Subcommand)]
pub(crate) enum ConfigSubcommand {
Init,
Show,
}
#[derive(Args)]
pub(crate) struct DaemonArgs {
#[command(subcommand)]
pub(crate) command: Option<DaemonSubcommand>,
}
#[derive(Subcommand)]
pub(crate) enum DaemonSubcommand {
Install(DaemonInstallArgs),
Uninstall,
Restart,
Repair(DaemonRepairArgs),
}
#[derive(Args)]
pub(crate) struct DaemonInstallArgs {
#[arg(
long,
value_name = "PATH",
help = "Write this executable path into the LaunchAgent"
)]
pub(crate) program: Option<PathBuf>,
}
#[derive(Args)]
pub(crate) struct DaemonRepairArgs {
#[arg(long, help = "Succeed without changes when the LaunchAgent is absent")]
pub(crate) if_installed: bool,
#[arg(
long,
value_name = "PATH",
help = "Write this executable path into the LaunchAgent"
)]
pub(crate) program: Option<PathBuf>,
}
#[derive(Args)]
pub(crate) struct ServerArgs {
#[arg(long, default_value = "127.0.0.1:0")]
pub(crate) bind: SocketAddr,
#[arg(long)]
pub(crate) data: PathBuf,
#[arg(long)]
pub(crate) unsafe_public_bind: bool,
}
#[derive(Args)]
pub(crate) struct SyncArgs {
#[arg(long)]
pub(crate) server: Option<String>,
}