Skip to main content

tix/cli/
cli_recurring.rs

1#[allow(clippy::wildcard_imports)]
2use super::*;
3
4/// What `tix recurring` does.
5///
6/// Every read-only subcommand resolves its date explicitly (`--date`), and every
7/// mutating one requires the confirmation flag it documents. The subcommands are a
8/// thin adapter: they parse, call one service, and print.
9#[derive(Debug, Clone, Subcommand)]
10pub enum RecurringAction {
11    /// List every series, one tab-separated row per series
12    ///
13    /// Columns: `id`, `name`, `state`, `next_cursor`, `skipped`.
14    List,
15    /// Show one series: identity, rule, state, cursor, skipped dates, occurrences
16    Show {
17        /// Stable series id, or its unique display name
18        series: String,
19    },
20    /// Create a series: the issue that *is* the definition
21    ///
22    /// The issue's own fields are the template an occurrence is created from, so
23    /// `--title`, `--labels` and the rest are also what each occurrence inherits.
24    /// The rule is validated **before** the issue is written.
25    Create(RecurringCreateArgs),
26    /// Change a series' name and/or rule, keeping its stable id
27    ///
28    /// Only the flags you name change; the rest of the rule is carried over, so
29    /// `tix recurring edit <id> --name X` is a rename and nothing else.
30    Edit {
31        /// Stable series id, or its unique display name
32        series: String,
33        #[command(flatten)]
34        args: RecurringEditArgs,
35    },
36    /// Report what is due, read-only, for an explicit date
37    Detect {
38        /// Evaluate against this `YYYY-MM-DD` date instead of today
39        #[arg(long, value_name = "YYYY-MM-DD")]
40        date: Option<String>,
41        /// Timezone policy to carry on the context, e.g. `+02:00`
42        #[arg(long, value_name = "TZ")]
43        timezone: Option<String>,
44    },
45    /// Create the occurrence for one due date; requires `--yes`
46    ///
47    /// Idempotent: the occurrence key is checked first, so a repeated call is a
48    /// no-op and never a second issue.
49    Generate {
50        /// Stable series id, or its unique display name
51        series: String,
52        /// The occurrence date, `YYYY-MM-DD`
53        #[arg(long, value_name = "YYYY-MM-DD")]
54        date: String,
55        #[command(flatten)]
56        confirmation: RecurringLifecycleArgs,
57    },
58    /// Suspend generation until an explicit resume
59    Pause {
60        /// Stable series id, or its unique display name
61        series: String,
62        #[command(flatten)]
63        confirmation: RecurringLifecycleArgs,
64    },
65    /// Resume a paused series; never inferred from a refresh
66    Resume {
67        /// Stable series id, or its unique display name
68        series: String,
69        #[command(flatten)]
70        confirmation: RecurringLifecycleArgs,
71    },
72    /// End a series: terminal for generation
73    End {
74        /// Stable series id, or its unique display name
75        series: String,
76        #[command(flatten)]
77        confirmation: RecurringLifecycleArgs,
78    },
79    /// Record one occurrence date as skipped, so it is never generated
80    Skip {
81        /// Stable series id, or its unique display name
82        series: String,
83        /// The occurrence date, `YYYY-MM-DD`
84        #[arg(long, value_name = "YYYY-MM-DD")]
85        date: String,
86        #[command(flatten)]
87        confirmation: RecurringLifecycleArgs,
88    },
89    /// Move the candidate past a date without recording it as skipped
90    Defer {
91        /// Stable series id, or its unique display name
92        series: String,
93        /// The occurrence date, `YYYY-MM-DD`
94        #[arg(long, value_name = "YYYY-MM-DD")]
95        date: String,
96        #[command(flatten)]
97        confirmation: RecurringLifecycleArgs,
98    },
99    /// Stop offering one occurrence date: recorded, like `skip`
100    Ignore {
101        /// Stable series id, or its unique display name
102        series: String,
103        /// The occurrence date, `YYYY-MM-DD`
104        #[arg(long, value_name = "YYYY-MM-DD")]
105        date: String,
106        #[command(flatten)]
107        confirmation: RecurringLifecycleArgs,
108    },
109}
110
111/// The rule flags `tix recurring create` and `edit` share.
112#[derive(Debug, Clone, Args)]
113pub struct RecurringRuleArgs {
114    /// Family: `daily`, `weekly`, `monthly`, `interval`, `cron-like`
115    #[arg(long, value_name = "KIND")]
116    pub rule: Option<String>,
117    /// Schedule origin, `YYYY-MM-DD` (required by every family)
118    #[arg(long, value_name = "YYYY-MM-DD")]
119    pub anchor: Option<String>,
120    /// Cadence, where the family takes one (1-1000)
121    #[arg(long, value_name = "N")]
122    pub interval: Option<u32>,
123    /// What an `interval` rule counts in: `days`, `weeks`, `months`
124    #[arg(long, value_name = "UNIT")]
125    pub unit: Option<String>,
126    /// Cron-like expression: `@daily`, `@weekly`, `@monthly`
127    #[arg(long, value_name = "EXPR")]
128    pub expression: Option<String>,
129    /// Timezone policy, e.g. `+02:00`
130    #[arg(long, value_name = "TZ")]
131    pub timezone: Option<String>,
132}
133
134impl RecurringRuleArgs {
135    /// Whether the caller named any rule field at all.
136    pub const fn is_any(&self) -> bool {
137        self.rule.is_some()
138            || self.anchor.is_some()
139            || self.interval.is_some()
140            || self.unit.is_some()
141            || self.expression.is_some()
142            || self.timezone.is_some()
143    }
144}
145
146/// The confirmation every mutating `tix recurring` subcommand takes.
147#[derive(Debug, Clone, Args)]
148pub struct RecurringLifecycleArgs {
149    /// Confirm the action without prompting
150    #[arg(long)]
151    pub yes: bool,
152    /// Why; recorded in the series' History
153    #[arg(long, value_name = "TEXT")]
154    pub reason: Option<String>,
155}
156
157/// The fields of the definition issue plus its rule.
158#[derive(Debug, Clone, Args)]
159pub struct RecurringCreateArgs {
160    /// Title of the series issue (and the default series name)
161    #[arg(long, value_name = "TITLE")]
162    pub title: String,
163    /// Body an occurrence inherits
164    #[arg(long, value_name = "BODY")]
165    pub body: Option<String>,
166    /// Column the definition (and its occurrences) starts in
167    #[arg(long, value_name = "STATUS")]
168    pub status: Option<String>,
169    /// Priority: `low`, `medium`, `high`
170    #[arg(long, value_name = "PRIORITY")]
171    pub priority: Option<String>,
172    /// Labels, comma-separated
173    #[arg(long, value_name = "LABELS")]
174    pub labels: Option<String>,
175    /// Assignee (`@name`)
176    #[arg(long, value_name = "ASSIGNEE")]
177    pub assignee: Option<String>,
178    /// Display name; defaults to the title
179    #[arg(long, value_name = "NAME")]
180    pub name: Option<String>,
181    /// Stable series id; generated when omitted
182    #[arg(long, value_name = "ID")]
183    pub id: Option<String>,
184    #[command(flatten)]
185    pub rule: RecurringRuleArgs,
186}
187
188/// What `tix recurring edit` may change.
189#[derive(Debug, Clone, Args)]
190pub struct RecurringEditArgs {
191    /// New display name; omitted means keep the current one
192    #[arg(long, value_name = "NAME")]
193    pub name: Option<String>,
194    #[command(flatten)]
195    pub rule: RecurringRuleArgs,
196}