tix/cli/cli_views.rs
1#[allow(clippy::wildcard_imports)]
2use super::*;
3
4/// The `tix view` saved-view subcommands (#172; mission
5/// `saved-issue-views-01M3C926`, WP04).
6///
7/// A **real** subcommand tree, not an optional-verb enum like [`LinkAction`]:
8/// there is no meaningful bare `tix view` (bare `tix view` would be the
9/// issue-printer, which takes an issue selector), so every saved-view operation
10/// names its verb. The nine verbs are the surface contract's spelling
11/// (`contracts/cli-surface.md`).
12///
13/// The plan-shaping flags are [`ViewPlanArgs`] and the scope flags are
14/// [`ViewScopeArgs`], flattened into the verbs that take them so `create`,
15/// `update`, `share`, and `status` cannot drift apart.
16#[derive(Subcommand)]
17pub enum ViewAction {
18 /// List saved views, in declaration order, as tab-separated rows
19 List {
20 /// Only views in this scope family: `repository` | `project` | `user`
21 #[arg(long, value_name = "repository|project|user")]
22 scope: Option<String>,
23 },
24 /// Show one saved view: stable id, name, scope, normalized plan, diagnostics
25 Show {
26 /// Stable view id, or its display name
27 reference: String,
28 },
29 /// Save a new view from the existing query grammar
30 ///
31 /// The id is **generated** and stable (spec §3: "use stable generated IDs;
32 /// names are display labels"); it is printed on the first stdout line so a
33 /// script can capture it.
34 Create {
35 /// Display label
36 #[arg(long, value_name = "NAME")]
37 name: String,
38 #[command(flatten)]
39 scope: ViewScopeArgs,
40 #[command(flatten)]
41 plan: ViewPlanArgs,
42 },
43 /// Replace an existing view's definition, keeping its stable id
44 ///
45 /// Only the fields you name change; the rest are carried over from the
46 /// stored definition, so `tix view update <id> --name X` is a rename and
47 /// nothing else (identity survives a rename, C-002).
48 Update {
49 /// Stable view id, or its display name
50 reference: String,
51 /// New display label; omitted means keep the current one
52 #[arg(long, value_name = "NAME")]
53 name: Option<String>,
54 #[command(flatten)]
55 plan: ViewPlanArgs,
56 },
57 /// Copy a view under a **new** stable id
58 Duplicate {
59 /// Stable view id, or its display name
60 reference: String,
61 /// Label for the copy; omitted means the source's label
62 #[arg(long, value_name = "NAME")]
63 name: Option<String>,
64 },
65 /// Remove a view **definition** only; requires `--confirm`
66 ///
67 /// Never deletes an issue, comment, or relationship.
68 Delete {
69 /// Stable view id, or its display name
70 reference: String,
71 /// Acknowledge that only the saved definition is removed
72 #[arg(long)]
73 confirm: bool,
74 },
75 /// Apply a saved view read-only: render its plan, result, and active marker
76 Apply {
77 /// Stable view id, or its display name
78 reference: String,
79 },
80 /// Report the saved plan against a live plan, field by field
81 ///
82 /// Without an id it reports the (absent) active marker — never a silent
83 /// "in sync". With an id, the live plan is assembled from the plan flags
84 /// given here, in the existing grammar.
85 Status {
86 /// Stable view id, or its display name; omitted reports the active marker
87 reference: Option<String>,
88 #[command(flatten)]
89 live: ViewPlanArgs,
90 },
91 /// Change a view's explicit sharing/visibility scope metadata
92 ///
93 /// Scope is metadata: `user` scope stores an owner/shared-with label and
94 /// grants no permission and creates no account (C-003/C-009).
95 Share {
96 /// Stable view id, or its display name
97 reference: String,
98 #[command(flatten)]
99 scope: ViewScopeArgs,
100 },
101}
102
103/// The explicit scope flags shared by `create` and `share`.
104///
105/// One flatten per verb rather than two hand-maintained copies, so a new scope
106/// field cannot be added to one and forgotten on the other.
107#[derive(Args, Debug, Clone, PartialEq, Eq)]
108pub struct ViewScopeArgs {
109 /// Scope family: `repository` (the default) | `project` | `user`
110 #[arg(long, value_name = "repository|project|user")]
111 pub scope: Option<String>,
112 /// Project id, for `--scope project`
113 ///
114 /// Resolved through the project registry; a project this repository does
115 /// not know is refused by the registry, not by this flag.
116 #[arg(long, value_name = "PROJECT")]
117 pub project: Option<String>,
118 /// Owner identity, for `--scope user` (metadata only — not an account)
119 #[arg(long, value_name = "IDENTITY")]
120 pub owner: Option<String>,
121 /// Comma-separated shared-with identities, for `--scope user`
122 #[arg(long, value_name = "LIST")]
123 pub shared_with: Option<String>,
124}
125
126/// The plan-shaping flags shared by `create`, `update`, and `status`.
127///
128/// The grammar is the **existing** one: `--filter` is the token query
129/// `tix list --filter` accepts, `--sort`/`--group` are that grammar's control
130/// tokens, and the visibility pair mirrors the board's `hidden_columns` /
131/// `show_mismatch`. Nothing here parses a second query language (C-004).
132#[derive(Args, Debug, Clone, PartialEq, Eq)]
133pub struct ViewPlanArgs {
134 /// Existing token query (`status:todo`, `label:urgent`, `category:done`, …)
135 #[arg(long, value_name = "QUERY")]
136 pub filter: Option<String>,
137 /// Sort key (`created`, `due`, `start`, `reviewer`, `cycle`, `milestone`,
138 /// `status`, `priority`)
139 #[arg(long, value_name = "KEY")]
140 pub sort: Option<String>,
141 /// Group key (`cycle` | `milestone`)
142 #[arg(long, value_name = "KEY")]
143 pub group: Option<String>,
144 /// Board columns to hide when the view is applied (comma-separated)
145 ///
146 /// Giving this **or** `--show-mismatch` makes visibility an absolute
147 /// snapshot; omitting both means the view inherits the live board's
148 /// visibility.
149 #[arg(long, value_name = "LIST")]
150 pub hide_columns: Option<String>,
151 /// Show the computed "Mismatched" column when applied (`true` | `false`)
152 #[arg(long, value_name = "true|false", num_args = 1, action = clap::ArgAction::Set)]
153 pub show_mismatch: Option<bool>,
154}
155
156impl ViewPlanArgs {
157 /// Whether the caller named **any** plan field.
158 ///
159 /// The question `update` asks: no field named means "leave the stored plan
160 /// alone", so a rename cannot silently wipe it.
161 #[must_use]
162 pub const fn is_any(&self) -> bool {
163 self.filter.is_some()
164 || self.sort.is_some()
165 || self.group.is_some()
166 || self.hide_columns.is_some()
167 || self.show_mismatch.is_some()
168 }
169}