tix-tracker 0.12.0

A terminal issue tracker: markdown + YAML frontmatter issues with a kanban TUI and CLI
Documentation
#[allow(clippy::wildcard_imports)]
use super::*;

/// The `tix view` saved-view subcommands (#172; mission
/// `saved-issue-views-01M3C926`, WP04).
///
/// A **real** subcommand tree, not an optional-verb enum like [`LinkAction`]:
/// there is no meaningful bare `tix view` (bare `tix view` would be the
/// issue-printer, which takes an issue selector), so every saved-view operation
/// names its verb. The nine verbs are the surface contract's spelling
/// (`contracts/cli-surface.md`).
///
/// The plan-shaping flags are [`ViewPlanArgs`] and the scope flags are
/// [`ViewScopeArgs`], flattened into the verbs that take them so `create`,
/// `update`, `share`, and `status` cannot drift apart.
#[derive(Subcommand)]
pub enum ViewAction {
    /// List saved views, in declaration order, as tab-separated rows
    List {
        /// Only views in this scope family: `repository` | `project` | `user`
        #[arg(long, value_name = "repository|project|user")]
        scope: Option<String>,
    },
    /// Show one saved view: stable id, name, scope, normalized plan, diagnostics
    Show {
        /// Stable view id, or its display name
        reference: String,
    },
    /// Save a new view from the existing query grammar
    ///
    /// The id is **generated** and stable (spec §3: "use stable generated IDs;
    /// names are display labels"); it is printed on the first stdout line so a
    /// script can capture it.
    Create {
        /// Display label
        #[arg(long, value_name = "NAME")]
        name: String,
        #[command(flatten)]
        scope: ViewScopeArgs,
        #[command(flatten)]
        plan: ViewPlanArgs,
    },
    /// Replace an existing view's definition, keeping its stable id
    ///
    /// Only the fields you name change; the rest are carried over from the
    /// stored definition, so `tix view update <id> --name X` is a rename and
    /// nothing else (identity survives a rename, C-002).
    Update {
        /// Stable view id, or its display name
        reference: String,
        /// New display label; omitted means keep the current one
        #[arg(long, value_name = "NAME")]
        name: Option<String>,
        #[command(flatten)]
        plan: ViewPlanArgs,
    },
    /// Copy a view under a **new** stable id
    Duplicate {
        /// Stable view id, or its display name
        reference: String,
        /// Label for the copy; omitted means the source's label
        #[arg(long, value_name = "NAME")]
        name: Option<String>,
    },
    /// Remove a view **definition** only; requires `--confirm`
    ///
    /// Never deletes an issue, comment, or relationship.
    Delete {
        /// Stable view id, or its display name
        reference: String,
        /// Acknowledge that only the saved definition is removed
        #[arg(long)]
        confirm: bool,
    },
    /// Apply a saved view read-only: render its plan, result, and active marker
    Apply {
        /// Stable view id, or its display name
        reference: String,
    },
    /// Report the saved plan against a live plan, field by field
    ///
    /// Without an id it reports the (absent) active marker — never a silent
    /// "in sync". With an id, the live plan is assembled from the plan flags
    /// given here, in the existing grammar.
    Status {
        /// Stable view id, or its display name; omitted reports the active marker
        reference: Option<String>,
        #[command(flatten)]
        live: ViewPlanArgs,
    },
    /// Change a view's explicit sharing/visibility scope metadata
    ///
    /// Scope is metadata: `user` scope stores an owner/shared-with label and
    /// grants no permission and creates no account (C-003/C-009).
    Share {
        /// Stable view id, or its display name
        reference: String,
        #[command(flatten)]
        scope: ViewScopeArgs,
    },
}

/// The explicit scope flags shared by `create` and `share`.
///
/// One flatten per verb rather than two hand-maintained copies, so a new scope
/// field cannot be added to one and forgotten on the other.
#[derive(Args, Debug, Clone, PartialEq, Eq)]
pub struct ViewScopeArgs {
    /// Scope family: `repository` (the default) | `project` | `user`
    #[arg(long, value_name = "repository|project|user")]
    pub scope: Option<String>,
    /// Project id, for `--scope project`
    ///
    /// Resolved through the project registry; a project this repository does
    /// not know is refused by the registry, not by this flag.
    #[arg(long, value_name = "PROJECT")]
    pub project: Option<String>,
    /// Owner identity, for `--scope user` (metadata only — not an account)
    #[arg(long, value_name = "IDENTITY")]
    pub owner: Option<String>,
    /// Comma-separated shared-with identities, for `--scope user`
    #[arg(long, value_name = "LIST")]
    pub shared_with: Option<String>,
}

/// The plan-shaping flags shared by `create`, `update`, and `status`.
///
/// The grammar is the **existing** one: `--filter` is the token query
/// `tix list --filter` accepts, `--sort`/`--group` are that grammar's control
/// tokens, and the visibility pair mirrors the board's `hidden_columns` /
/// `show_mismatch`. Nothing here parses a second query language (C-004).
#[derive(Args, Debug, Clone, PartialEq, Eq)]
pub struct ViewPlanArgs {
    /// Existing token query (`status:todo`, `label:urgent`, `category:done`, …)
    #[arg(long, value_name = "QUERY")]
    pub filter: Option<String>,
    /// Sort key (`created`, `due`, `start`, `reviewer`, `cycle`, `milestone`,
    /// `status`, `priority`)
    #[arg(long, value_name = "KEY")]
    pub sort: Option<String>,
    /// Group key (`cycle` | `milestone`)
    #[arg(long, value_name = "KEY")]
    pub group: Option<String>,
    /// Board columns to hide when the view is applied (comma-separated)
    ///
    /// Giving this **or** `--show-mismatch` makes visibility an absolute
    /// snapshot; omitting both means the view inherits the live board's
    /// visibility.
    #[arg(long, value_name = "LIST")]
    pub hide_columns: Option<String>,
    /// Show the computed "Mismatched" column when applied (`true` | `false`)
    #[arg(long, value_name = "true|false", num_args = 1, action = clap::ArgAction::Set)]
    pub show_mismatch: Option<bool>,
}

impl ViewPlanArgs {
    /// Whether the caller named **any** plan field.
    ///
    /// The question `update` asks: no field named means "leave the stored plan
    /// alone", so a rename cannot silently wipe it.
    #[must_use]
    pub const fn is_any(&self) -> bool {
        self.filter.is_some()
            || self.sort.is_some()
            || self.group.is_some()
            || self.hide_columns.is_some()
            || self.show_mismatch.is_some()
    }
}