use std::num::NonZeroU32;
use clap::{Args, Parser, Subcommand, ValueEnum};
use onetaskgraph_core::config::{Layer, Origin, Setting, SettingPath, value_from_text};
use onetaskgraph_core::{OutputFormat, PluginKind, SearchKind};
use onetaskgraph_plugin_api::{Direction, StatusCategory, TextFields};
use serde_json::Value;
#[derive(Debug, Parser)]
#[command(name = "onetaskgraph", bin_name = "onetaskgraph", version)]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
#[command(flatten)]
pub overrides: Overrides,
}
#[derive(Debug, Subcommand)]
pub enum Command {
#[command(hide = true)]
PluginServe {
source: PluginKind,
},
Schema,
Config {
#[command(subcommand)]
command: ConfigCommand,
},
Sources {
#[command(subcommand)]
command: SourcesCommand,
},
Task {
#[command(subcommand)]
command: TaskCommand,
},
Project {
#[command(subcommand)]
command: ProjectCommand,
},
Label {
#[command(subcommand)]
command: LabelCommand,
},
Search(SearchArgs),
}
#[derive(Debug, Subcommand)]
pub enum SourcesCommand {
List,
}
#[derive(Debug, Subcommand)]
pub enum TaskCommand {
List(TaskListArgs),
Show(ShowArgs),
Deps(DependencyArgs),
Copy(TaskCopyArgs),
}
#[derive(Debug, Subcommand)]
pub enum ProjectCommand {
List(ProjectListArgs),
Show(ShowArgs),
Deps(DependencyArgs),
Copy(ProjectCopyArgs),
}
#[derive(Debug, Subcommand)]
pub enum LabelCommand {
List(LabelListArgs),
}
#[derive(Debug, Args)]
pub struct SelectionArgs {
#[arg(long = "source", value_name = "S")]
pub source: Vec<String>,
}
#[derive(Debug, Args)]
pub struct FilterArgs {
#[arg(long = "label", value_name = "L")]
pub label: Vec<String>,
#[arg(long = "not-label", value_name = "L")]
pub not_label: Vec<String>,
#[arg(long = "status", value_name = "S")]
pub status: Vec<StatusArg>,
#[arg(long, value_name = "TEXT")]
pub search: Option<String>,
#[arg(long = "in", value_name = "FIELDS", default_value = "both")]
pub fields: FieldsArg,
}
#[derive(Debug, Args)]
pub struct PageArgs {
#[arg(long, value_name = "N")]
pub limit: Option<NonZeroU32>,
#[arg(long = "page", value_name = "TOKEN")]
pub page: Option<String>,
#[arg(long)]
pub explain: bool,
#[arg(long = "allow-partial")]
pub allow_partial: bool,
}
#[derive(Debug, Args)]
pub struct TaskListArgs {
#[command(flatten)]
pub selection: SelectionArgs,
#[command(flatten)]
pub filters: FilterArgs,
#[arg(long, value_name = "P", conflicts_with = "no_project")]
pub project: Option<String>,
#[arg(long = "no-project")]
pub no_project: bool,
#[command(flatten)]
pub paging: PageArgs,
}
#[derive(Debug, Args)]
pub struct ProjectListArgs {
#[command(flatten)]
pub selection: SelectionArgs,
#[command(flatten)]
pub filters: FilterArgs,
#[command(flatten)]
pub paging: PageArgs,
}
#[derive(Debug, Args)]
pub struct LabelListArgs {
#[command(flatten)]
pub selection: SelectionArgs,
#[command(flatten)]
pub paging: PageArgs,
}
#[derive(Debug, Args)]
pub struct ShowArgs {
#[arg(value_name = "ID")]
pub id: String,
#[arg(long)]
pub explain: bool,
#[arg(long = "allow-partial")]
pub allow_partial: bool,
}
#[derive(Debug, Args)]
pub struct DependencyArgs {
#[arg(value_name = "ID")]
pub id: String,
#[arg(long, value_name = "DIRECTION", default_value = "depends-on")]
pub direction: DirectionArg,
#[command(flatten)]
pub paging: PageArgs,
}
#[derive(Debug, Args)]
pub struct CopyArgs {
#[arg(long, value_name = "SOURCE")]
pub to: String,
#[arg(long = "match-by", value_name = "KEY")]
pub match_by: Option<String>,
#[arg(long)]
pub recreate: bool,
#[arg(long = "dry-run")]
pub dry_run: bool,
}
#[derive(Debug, Args)]
pub struct TaskCopyArgs {
#[arg(value_name = "ID", required = true)]
pub id: Vec<String>,
#[command(flatten)]
pub copy: CopyArgs,
}
#[derive(Debug, Args)]
pub struct ProjectCopyArgs {
#[arg(value_name = "ID")]
pub id: String,
#[arg(long = "no-tasks")]
pub no_tasks: bool,
#[command(flatten)]
pub copy: CopyArgs,
}
#[derive(Debug, Args)]
pub struct SearchArgs {
#[arg(value_name = "TEXT")]
pub text: String,
#[arg(long = "in", value_name = "FIELDS", default_value = "both")]
pub fields: FieldsArg,
#[arg(long, value_name = "KIND", default_value = "both")]
pub kind: KindArg,
#[command(flatten)]
pub selection: SelectionArgs,
#[command(flatten)]
pub paging: PageArgs,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum StatusArg {
Backlog,
Todo,
InProgress,
Done,
Cancelled,
Unknown,
}
impl StatusArg {
#[must_use]
pub fn category(self) -> StatusCategory {
match self {
Self::Backlog => StatusCategory::Backlog,
Self::Todo => StatusCategory::Todo,
Self::InProgress => StatusCategory::InProgress,
Self::Done => StatusCategory::Done,
Self::Cancelled => StatusCategory::Cancelled,
Self::Unknown => StatusCategory::Unknown,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum FieldsArg {
Title,
Content,
Both,
}
impl FieldsArg {
#[must_use]
pub fn fields(self) -> TextFields {
match self {
Self::Title => TextFields::Title,
Self::Content => TextFields::Content,
Self::Both => TextFields::TitleOrContent,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum DirectionArg {
DependsOn,
DependedOnBy,
}
impl DirectionArg {
#[must_use]
pub fn direction(self) -> Direction {
match self {
Self::DependsOn => Direction::DependsOn,
Self::DependedOnBy => Direction::DependedOnBy,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum KindArg {
Task,
Project,
Both,
}
impl KindArg {
#[must_use]
pub fn kind(self) -> SearchKind {
match self {
Self::Task => SearchKind::Tasks,
Self::Project => SearchKind::Projects,
Self::Both => SearchKind::Both,
}
}
}
#[derive(Debug, Subcommand)]
pub enum ConfigCommand {
Show,
}
#[derive(Debug, Args)]
pub struct Overrides {
#[arg(long = "set", value_name = "PATH=VALUE", global = true)]
pub set: Vec<String>,
#[arg(
long,
value_name = "N",
global = true,
value_parser = clap::value_parser!(u32).range(1..)
)]
pub page_size: Option<u32>,
#[arg(long, value_name = "NAMES", value_delimiter = ',', global = true)]
pub default_sources: Option<Vec<String>>,
#[arg(long, value_name = "FORMAT", global = true, conflicts_with = "json")]
pub output: Option<Format>,
#[arg(long, global = true)]
pub json: bool,
}
impl Overrides {
pub fn layer(&self) -> Result<Layer, String> {
let mut settings = Vec::new();
if let Some(page_size) = self.page_size {
settings.push(at("page_size", Value::from(page_size), "--page-size"));
}
if let Some(names) = &self.default_sources {
let names: Vec<Value> = names.iter().map(|name| Value::from(name.clone())).collect();
settings.push(at(
"default_sources",
Value::Array(names),
"--default-sources",
));
}
if let Some(format) = self.output {
settings.push(at("output", format.setting(), "--output"));
}
if self.json {
settings.push(at("output", Value::from("json"), "--json"));
}
for assignment in &self.set {
settings.push(assignment_setting(assignment)?);
}
Ok(Layer::new(settings))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum Format {
Text,
Json,
}
impl Format {
#[must_use]
pub fn setting(self) -> Value {
let format = match self {
Self::Text => OutputFormat::Text,
Self::Json => OutputFormat::Json,
};
serde_json::to_value(format).expect("an output format renders as JSON")
}
}
fn at(key: &str, value: Value, flag: &str) -> Setting {
Setting {
key: SettingPath::parse(key).expect("a path this binary spells has no empty segment"),
value,
origin: Origin::Flag {
flag: flag.to_owned(),
},
}
}
fn assignment_setting(assignment: &str) -> Result<Setting, String> {
let Some((path, value)) = assignment.split_once('=') else {
return Err(format!(
"--set {assignment}: that is not an assignment\n\
next: write it as --set PATH=VALUE, for example --set page_size=10."
));
};
Ok(Setting {
key: SettingPath::parse(path).map_err(|error| format!("--set {error}"))?,
value: value_from_text(value),
origin: Origin::Flag {
flag: format!("--set {path}"),
},
})
}