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::*;

/// `tix bulk <action>` — four actions, and each carries **its own** flags.
///
/// The flags are on the subcommand rather than on the parent, which is what makes the
/// surface unambiguous by construction rather than by a runtime check:
/// `tix bulk archive --priority high` is a **clap usage error**, not a command that
/// accepts a field flag and ignores it. `edit` is the only action with field flags,
/// `link`/`unlink` are the only ones with `--target`, and `archive` takes neither — so
/// there is no spelling of `tix bulk` that a flag can apply to by accident.
///
/// `archive` has no `--type`: archiving is not a relationship, and offering a kind
/// would suggest it is one.
/// The flags every `tix bulk` action carries: which issues, and whether to write.
///
/// Flattened into **each** action rather than held on `Command::Bulk`, because clap
/// parses a parent's flags *before* its subcommand — so a parent would spell the
/// contract's `tix bulk edit --select <...>` as `tix bulk --select <...> edit`, which
/// is a different surface from the one the contract names. Four `#[command(flatten)]`
/// of one struct gives the contract's spelling and one vocabulary, so a new flag
/// cannot be added to one action and forgotten on the other three.
#[derive(Args, Debug, Clone, PartialEq, Eq)]
pub struct BulkSelection {
    /// Comma-separated issue selectors: number, filename fragment or unique title
    /// fragment
    #[arg(long, value_name = "LIST")]
    pub select: Option<String>,
    /// A `--filter` query, in the existing filter grammar
    #[arg(long, value_name = "QUERY")]
    pub filter: Option<String>,
    /// A `--search` query, in the existing search grammar
    #[arg(long, value_name = "QUERY")]
    pub search: Option<String>,
    /// Which issues are in play: `active`, `active+archived` or `archived`
    #[arg(long, value_name = "SCOPE")]
    pub scope: Option<String>,
    /// Write the batch. Omit for a preview, which is the default.
    #[arg(long)]
    pub apply: bool,
}

impl BulkAction {
    /// The shared selection flags, whichever action carries them.
    ///
    /// One accessor for four flattened fields, so the routing code reads the flags
    /// once and a new action cannot forget to pass them.
    pub fn selection(&self) -> &BulkSelection {
        match self {
            Self::Edit { selection, .. }
            | Self::Link { selection, .. }
            | Self::Unlink { selection, .. }
            | Self::Archive { selection } => selection,
        }
    }

    /// Whether `--apply` was given. A preview is the default, so this is a positive
    /// question rather than "is this not a preview".
    pub fn is_apply(&self) -> bool {
        self.selection().apply
    }
}

#[derive(Subcommand, Debug, Clone, PartialEq, Eq)]
pub enum BulkAction {
    /// Apply a typed field patch to every selected issue
    Edit {
        #[command(flatten)]
        selection: BulkSelection,
        /// Target column
        #[arg(long, value_name = "COLUMN")]
        status: Option<String>,
        /// `low`, `medium`, `high`, or `` for unset
        #[arg(long, value_name = "PRIORITY")]
        priority: Option<String>,
        /// Bare handle or full identity
        #[arg(long, value_name = "ASSIGNEE")]
        assignee: Option<String>,
        /// Comma-separated labels
        #[arg(long, value_name = "LABELS")]
        labels: Option<String>,
        /// `YYYY-MM-DD`
        #[arg(long, value_name = "DATE")]
        start_date: Option<String>,
        /// `YYYY-MM-DD`
        #[arg(long, value_name = "DATE")]
        due_date: Option<String>,
        /// Estimate: `3`, `3d`, `1 day`, `0.5-1 day`, `~300 LOC, 0.5-1 day`
        #[arg(long, value_name = "ESTIMATE")]
        estimate: Option<String>,
        /// Comma-separated cycle names
        #[arg(long, value_name = "CYCLES")]
        cycles: Option<String>,
        /// Comma-separated milestone names
        #[arg(long, value_name = "MILESTONES")]
        milestones: Option<String>,
        /// Empty a field. Takes one of the clearable field names.
        #[arg(long, value_name = "FIELD")]
        clear: Option<String>,
    },
    /// Link every selected issue to `--target`
    Link {
        #[command(flatten)]
        selection: BulkSelection,
        /// The other end of the relationship
        #[arg(long, value_name = "SELECTOR")]
        target: String,
        /// Relationship kind: `blocks`, `blocked_by`, `duplicates`, `related`
        #[arg(long, value_name = "KIND")]
        r#type: Option<String>,
    },
    /// Unlink every selected issue from `--target`
    Unlink {
        #[command(flatten)]
        selection: BulkSelection,
        /// The other end of the relationship
        #[arg(long, value_name = "SELECTOR")]
        target: String,
        /// Relationship kind: `blocks`, `blocked_by`, `duplicates`, `related`
        #[arg(long, value_name = "KIND")]
        r#type: Option<String>,
    },
    /// Ask the archive service to archive every selected issue
    Archive {
        #[command(flatten)]
        selection: BulkSelection,
    },
}