use super::complete::{active_pads_completer, all_pads_completer, deleted_pads_completer};
use clap::{CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum};
use once_cell::sync::Lazy;
use outstanding::topics::TopicRegistry;
use outstanding::OutputMode;
use outstanding_clap::{render_help_with_topics, Outstanding};
#[derive(Copy, Clone, Debug, ValueEnum)]
pub enum CompletionShell {
Bash,
Zsh,
}
fn get_version() -> &'static str {
const VERSION: &str = env!("CARGO_PKG_VERSION");
const GIT_HASH: &str = env!("GIT_HASH");
const GIT_COMMIT_DATE: &str = env!("GIT_COMMIT_DATE");
const IS_RELEASE: &str = env!("IS_RELEASE");
use std::sync::OnceLock;
static VERSION_STRING: OnceLock<String> = OnceLock::new();
VERSION_STRING.get_or_init(|| {
if IS_RELEASE == "true" {
format!("v{}", VERSION)
} else {
format!("v{}\ndev: {} {}", VERSION, GIT_HASH, GIT_COMMIT_DATE)
}
})
}
#[derive(Parser, Debug)]
#[command(
name = "padz",
bin_name = "padz",
version = get_version(),
disable_help_subcommand = true,
after_help = "Enable shell completions:\n eval \"$(padz completions bash)\" # add to ~/.bashrc\n eval \"$(padz completions zsh)\" # add to ~/.zshrc"
)]
#[command(about = "Context-aware command-line note-taking tool", long_about = None)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Commands>,
#[arg(short, long, global = true, help_heading = "Options")]
pub global: bool,
#[arg(short, long, global = true, help_heading = "Options")]
pub verbose: bool,
}
static HELP_TOPICS: Lazy<TopicRegistry> = Lazy::new(|| {
let mut registry = TopicRegistry::new();
let topic_content = include_str!("topics/scopes.txt");
if let Some(topic) = parse_topic_file("scopes", topic_content) {
registry.add_topic(topic);
}
registry
});
fn parse_topic_file(name: &str, content: &str) -> Option<outstanding::topics::Topic> {
let lines: Vec<&str> = content.lines().collect();
if lines.len() < 2 {
return None;
}
let title_idx = lines.iter().position(|l| !l.trim().is_empty())?;
let title = lines[title_idx].trim().to_string();
let content_lines = &lines[title_idx + 1..];
let content_start = content_lines
.iter()
.position(|l| !l.trim().is_empty())
.unwrap_or(content_lines.len());
let body = content_lines[content_start..].join("\n");
if body.trim().is_empty() {
return None;
}
Some(outstanding::topics::Topic::new(
title,
body,
outstanding::topics::TopicType::Text,
Some(name.to_string()),
))
}
pub fn build_command() -> clap::Command {
Cli::command()
}
pub fn parse_cli() -> (Cli, OutputMode) {
let outstanding = Outstanding::with_registry(HELP_TOPICS.clone());
let matches = outstanding.run_with(Cli::command());
let output_mode = match matches
.get_one::<String>("_output_mode")
.map(|s| s.as_str())
{
Some("term") => OutputMode::Term,
Some("text") => OutputMode::Text,
Some("term-debug") => OutputMode::TermDebug,
Some("json") => OutputMode::Json,
_ => OutputMode::Auto,
};
let cli = Cli::from_arg_matches(&matches).expect("Failed to parse CLI arguments");
(cli, output_mode)
}
pub fn get_grouped_help() -> String {
let cmd = Cli::command();
render_help_with_topics(&cmd, &HELP_TOPICS, None).unwrap_or_else(|_| {
let version = cmd.get_version().unwrap_or("unknown");
format!("padz {version}\nContext-aware command-line note-taking tool\n\nUsage: padz [OPTIONS] [COMMAND]\n")
})
}
#[derive(Subcommand, Debug)]
pub enum Commands {
#[command(flatten)]
Core(CoreCommands),
#[command(flatten)]
Pad(PadCommands),
#[command(flatten)]
Data(DataCommands),
#[command(flatten)]
Misc(MiscCommands),
}
#[derive(Subcommand, Debug)]
pub enum CoreCommands {
#[command(alias = "n", display_order = 1)]
Create {
#[arg(long)]
no_editor: bool,
#[arg(long, short = 'i')]
inside: Option<String>,
#[arg(trailing_var_arg = true)]
title: Vec<String>,
},
#[command(alias = "ls", display_order = 2)]
List {
#[arg(short, long)]
search: Option<String>,
#[arg(long)]
deleted: bool,
#[arg(long)]
peek: bool,
#[arg(long, conflicts_with_all = ["done", "in_progress"])]
planned: bool,
#[arg(long, conflicts_with_all = ["planned", "in_progress"])]
done: bool,
#[arg(long, conflicts_with_all = ["planned", "done"])]
in_progress: bool,
},
#[command(display_order = 3)]
Search { term: String },
}
#[derive(Subcommand, Debug)]
pub enum PadCommands {
#[command(alias = "v", display_order = 10)]
View {
#[arg(required = true, num_args = 1.., add = all_pads_completer())]
indexes: Vec<String>,
#[arg(long)]
peek: bool,
},
#[command(alias = "e", display_order = 11)]
Edit {
#[arg(required = true, num_args = 1.., add = active_pads_completer())]
indexes: Vec<String>,
},
#[command(alias = "o", display_order = 12)]
Open {
#[arg(required = true, num_args = 1.., add = all_pads_completer())]
indexes: Vec<String>,
},
#[command(alias = "rm", display_order = 13)]
Delete {
#[arg(num_args = 1.., add = active_pads_completer(), required_unless_present = "done_status")]
indexes: Vec<String>,
#[arg(long = "done", conflicts_with = "indexes")]
done_status: bool,
},
#[command(display_order = 14)]
Restore {
#[arg(required = true, num_args = 1.., add = deleted_pads_completer())]
indexes: Vec<String>,
},
#[command(alias = "p", display_order = 15)]
Pin {
#[arg(required = true, num_args = 1.., add = active_pads_completer())]
indexes: Vec<String>,
},
#[command(alias = "u", display_order = 16)]
Unpin {
#[arg(required = true, num_args = 1.., add = active_pads_completer())]
indexes: Vec<String>,
},
#[command(alias = "mv", display_order = 13)]
Move {
#[arg(required = true, num_args = 1.., add = active_pads_completer())]
indexes: Vec<String>,
#[arg(long, short = 'r')]
root: bool,
},
#[command(display_order = 17)]
Path {
#[arg(required = true, num_args = 1.., add = all_pads_completer())]
indexes: Vec<String>,
},
#[command(alias = "done", display_order = 18)]
Complete {
#[arg(required = true, num_args = 1.., add = active_pads_completer())]
indexes: Vec<String>,
},
#[command(display_order = 19)]
Reopen {
#[arg(required = true, num_args = 1.., add = active_pads_completer())]
indexes: Vec<String>,
},
}
#[derive(Subcommand, Debug)]
pub enum DataCommands {
#[command(display_order = 20)]
Purge {
#[arg(required = false, num_args = 0.., add = deleted_pads_completer())]
indexes: Vec<String>,
#[arg(long, short = 'y')]
yes: bool,
#[arg(long, short = 'r')]
recursive: bool,
},
#[command(display_order = 21)]
Export {
#[arg(long, value_name = "TITLE")]
single_file: Option<String>,
#[arg(required = false, num_args = 0.., add = active_pads_completer())]
indexes: Vec<String>,
},
#[command(display_order = 22)]
Import {
#[arg(required = true, num_args = 1..)]
paths: Vec<String>,
},
}
#[derive(Subcommand, Debug)]
pub enum MiscCommands {
#[command(display_order = 30)]
Doctor,
#[command(display_order = 31)]
Config {
key: Option<String>,
value: Option<String>,
},
#[command(display_order = 32)]
Init,
#[command(display_order = 34)]
Completions {
#[arg(value_enum)]
shell: CompletionShell,
},
}