Skip to main content

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}