Skip to main content

tix/app/
mode.rs

1#[allow(clippy::wildcard_imports)]
2use super::*;
3
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub enum Mode {
6    Board,
7    /// The batch modal (WP07). The modal's state is in [`App::bulk`]; the mode says
8    /// only that it is open, exactly as `Mode::FilterModal` does.
9    BulkModal,
10    /// The triage inbox (WP06, T029). A modal over the board like settings and
11    /// the batch modal, so the board, its tabs and its card cursor are all
12    /// still there underneath and unchanged when it closes.
13    ///
14    /// The inbox's own cursor and selection live in [`App::triage`] rather than
15    /// in [`BoardState`], which is what makes "the normal board/detail
16    /// selection is unchanged when the inbox is not active" a property of
17    /// which struct each cursor is in rather than something a test has to
18    /// remember to restore.
19    TriageInbox,
20    /// The F3 Cycle mode (tui-cycle-mode): cycles grouped current / future /
21    /// archived with statistics for the selected one. Like the triage inbox it
22    /// is the mode's content, not a modal over the board; the
23    /// planning-management surface is reached from it with `m`.
24    Cycle,
25    /// The all-issues mentions view (OpenSpec change `issue-mentions`): every
26    /// pending `@mention` the local user holds, across issues. A modal over the
27    /// board like the batch modal, so the cards stay on screen underneath.
28    ///
29    /// Its state is [`crate::ui::mentions::MentionsView`], held as
30    /// [`App::mentions_view`] — `Some` exactly while it is open, so a closed view
31    /// has no rows to act on.
32    Mentions,
33    /// The per-issue mention notice (OpenSpec change `issue-mentions`): the issue
34    /// just opened carries unread mentions for the local user.
35    ///
36    /// Deliberately **non-mutating**: dismissing it (Esc, or `i` for "ignore")
37    /// writes nothing, which is what makes the mention reappear the next time the
38    /// issue is opened. Only "mark read" reaches the store. Its keys are raw (see
39    /// `handle_key`), for the reason the recurring warning's are: the board binds
40    /// some of the same letters and the binding layer would rewrite them first.
41    MentionNotice,
42    /// The recurring-issue due warning (mission `recurring-issues-01M3C7G7`,
43    /// WP07): a non-mutating overlay over the board, `Some` exactly while it is
44    /// open. Its state is [`crate::ui::recurring_warning::RecurringWarning`].
45    ///
46    /// The rows come from the shared detector, and each of the four answers is an
47    /// explicit press — the overlay itself never generates, defers or ignores
48    /// anything, which is what makes "detection is read-only" true in the TUI.
49    /// Its keys are raw (see `handle_key`): the board's `d`/`g` bindings would
50    /// rewrite them into `Esc`.
51    RecurringWarning,
52    /// The WIP override confirmation (mission `configurable-wip-limits-01M3C6WX`,
53    /// WP05 T026): a blocking limit refused a move and the user is supplying the
54    /// reason that authorizes it. An overlay over the board, like the due warning;
55    /// it carries the one free-text field, so the reason is typed rather than
56    /// pressed.
57    WipOverride {
58        /// The reason being typed.
59        buffer: String,
60    },
61    /// The F7 Stats page's **Health tab** (mission
62    /// `project-issue-health-status-01M3C8HX`, WP06): one row per scope, with its
63    /// state, layer and derived value, and the diagnostics below. Read-only — the
64    /// mutations live in the detail view's form.
65    Stats,
66    /// The health form (mission `project-issue-health-status-01M3C8HX`, WP06 T032):
67    /// the audit fields a mutation needs, opened from the detail pane. An overlay
68    /// over the pane, like the WIP override: it carries free text, so `Tab` walks the
69    /// fields and `Enter` submits.
70    HealthForm,
71    /// The time-entry form (mission `per-issue-time-tracking-01M3C9A4`, WP05 T025):
72    /// the audit fields an add, a correction or a deletion needs, opened from the `F7`
73    /// Stats page's Time tab. An overlay over the page, like the health form: it
74    /// carries free text, so `Tab` walks the fields and `Enter` submits.
75    ///
76    /// The form's own state is [`App::time_form`], and the tab underneath keeps
77    /// drawing, so the entry rows the correction is about stay on screen while its
78    /// fields are typed.
79    TimeForm,
80    /// The Time tab's filter editor (WP05 T026): the issue, assignee and period
81    /// criteria the shared `TimeFilter` is built from. A separate mode only because it
82    /// is a separate field set — the keys (`Tab`/`Enter`/`Esc`) are the time form's.
83    TimeFilterForm,
84    /// The F8 Schedule page (tix #1593): the recurring series and their state on its
85    /// **Series** tab, and the configured cycle schedules with the cycles they
86    /// generated on its **Cycle schedules** tab (mission
87    /// `repeating-cycle-schedules-01M3C7W1`, tix #181), each with the keys that act
88    /// on them. A top-level mode like `F4`/`F5`/`F6`.
89    Schedule,
90    /// The F6 Repos mode (tix #1529): the child (and parent) repositories this
91    /// project coordinates. `j`/`k` walk the rows and `Enter` runs `tix` inside
92    /// the selected repository. With none configured, the body explains how to
93    /// add one under `[repos]`.
94    Repos,
95    /// The F11 Library page (tix #1593): the saved views and the issue/form
96    /// templates that already exist, listed with the selected record's detail.
97    /// `Tab` switches registries, `j`/`k` walk rows, and `Enter` applies the
98    /// selected view to the board. Read-only apart from that apply — see
99    /// [`crate::ui::library`].
100    Library,
101    /// The F10 **Portfolio** page (WP06): the project registry this mission
102    /// introduces, listed with each project's derived member count and the selected
103    /// project's record and members beside it. Read-only — see
104    /// [`crate::ui::portfolio`].
105    Portfolio,
106    RolloverDialog,
107    Detail,
108    Form,
109    ConfirmDelete,
110    /// Archive or restore one issue, or every marked issue, after a `y`/`N`
111    /// confirmation.
112    ConfirmArchive {
113        filenames: Vec<String>,
114        restore: bool,
115    },
116    MoveIntoDone {
117        buffer: String,
118    },
119    /// The blocked-move acknowledgement prompt (WP04, FR-004).
120    ///
121    /// Reached instead of applying a move toward the board's completion column
122    /// when the issue has open blockers. `warning` is the **shared** text from
123    /// `commands::blocked_note`, so the TUI says exactly what the CLI says.
124    ConfirmBlockedMove {
125        warning: String,
126    },
127    /// The reasoned-override prompt (WP05, FR-002, contract §3.3).
128    ///
129    /// Reached instead of applying a move that leaves a **terminal** category
130    /// when the board declares a `category_map`. `reason` is the **shared**
131    /// diagnostic from the service, so the TUI says exactly what the CLI says —
132    /// parity includes the message, not only the accept/refuse decision.
133    ///
134    /// This prompt is the whole reason WP05 exists on this path. Until it, a
135    /// terminal-exit transition was refused by the seam with a message naming
136    /// `tix move … --reason`, and the TUI had **no way to supply one** — the flag
137    /// is a CLI spelling and the TUI has no flags. So a user who moved an issue to
138    /// `done` in the TUI could not move it back, in the TUI, at all.
139    TransitionReason {
140        /// The refusal being overridden, rendered verbatim.
141        reason: String,
142        /// What the user has typed so far.
143        buffer: String,
144    },
145    Help {
146        return_to: Box<Mode>,
147    },
148    /// The startup tips modal (tix #1632): one tip from the embedded
149    /// [`crate::tips`] list, over the board opened by bare `tix`. `return_to` is
150    /// the mode it was opened from, so closing it lands exactly where the session
151    /// was headed.
152    ///
153    /// Its own state is [`App::tips`], `Some` exactly while it is open. Its keys
154    /// are raw (see `handle_key`): the board binds some of the same letters, and
155    /// the binding layer would rewrite them first.
156    Tips {
157        return_to: Box<Mode>,
158    },
159    /// The issue action menu (`Enter`, a plain left click, or right-click);
160    /// `return_to` is the mode it was opened from. Activation runs the exact
161    /// action the equivalent key binding would.
162    ActionMenu {
163        return_to: Box<Mode>,
164    },
165    Filter {
166        buffer: String,
167    },
168    FilterModal,
169    /// The saved-views modal (WP05, T024–T027). A modal over the board like
170    /// settings and the batch modal, so the board, its tabs, and its card cursor
171    /// are all still there underneath and unchanged when it closes.
172    ///
173    /// Its own state lives in [`App::views`] rather than in `BoardState`, so
174    /// "navigation is unchanged when the view list is not open" is a property of
175    /// which struct holds the cursor rather than something a test remembers to
176    /// restore.
177    Views,
178    /// The F12 Config mode (tui-config-mode): the sectioned surface over
179    /// dashboards, cycles, and milestones. Like the triage inbox and the cycle
180    /// listing it is the mode's content, not a modal over the board. Its state
181    /// is [`App::settings`], and its cycles/milestones sections drive
182    /// [`App::planning`] too.
183    Settings,
184    PlanningManagement,
185    PlanningForm,
186    /// Calendar date picker popup; `return_to` is the modal it overlays.
187    DatePicker {
188        return_to: Box<Mode>,
189    },
190    Comment {
191        buffer: String,
192    },
193    Asset {
194        buffer: String,
195    },
196    /// The editor round trip left a file with a problem (tix #1506).
197    ///
198    /// One prompt for every post-edit problem: a file that will not parse, a
199    /// file `tix lint` is unhappy about, or an edit that lost its title. It
200    /// lists each problem with its diagnostic and a proposal, and offers the fix
201    /// tix can apply, the editor again on the retained copy, an explicit
202    /// declaration of an undeclared key, acknowledgement, and a discard that
203    /// needs a second confirmation.
204    ///
205    /// Nothing is discarded automatically: the retained copy (`temp`) survives
206    /// acknowledgement and is only removed by the confirmed discard. When
207    /// `written` is true the edited content is already on disk (`tix lint`
208    /// findings arrive after the write, tix #1510's #1506 behaviour), so the
209    /// prompt is a report; when it is false nothing has been written and the fix
210    /// writes the repaired file for the first time.
211    ///
212    /// `key` is the undeclared top-level frontmatter key the prompt can declare,
213    /// `Some` only for an `unknown_key` problem with a non-empty value. It is what
214    /// `confirm`/`declare_key` write into `tix.toml`; every other problem leaves
215    /// it `None`, so those chords say there is nothing to declare.
216    EditRepair {
217        filename: String,
218        /// `kind: message` of the first problem, for the status line and the
219        /// prompt's heading.
220        message: String,
221        key: Option<String>,
222        /// Every classified problem, in the order the diagnostics arrived.
223        problems: Vec<EditProblem>,
224        /// The retained edited copy — the editor's temp file when one exists.
225        /// The only thing a confirmed discard removes.
226        temp: Option<PathBuf>,
227        /// Whether the edited content is already on disk.
228        written: bool,
229        /// A discard has been asked for once; the second keypress is the
230        /// decision. `Esc` never sets it.
231        discard_armed: bool,
232    },
233    /// The issue file changed on disk while the editor had it open (tix #1510).
234    ///
235    /// Entered **instead of** the write, so nothing about the raced edit is on
236    /// disk: the newer file is untouched and the editor's version is still the
237    /// temp copy. The three keys the prompt names are the only way out, and two of
238    /// them — `a`/`Esc` (keep the on-disk version) and `c` (compare in the
239    /// editor) — never write the editor's version at all. Only `o`/`y` overrides.
240    EditConflict {
241        /// The issue file, relative to the store's `issues_dir`.
242        filename: String,
243        /// The edited temp copy — the editor's version, kept until the user
244        /// decides (abandoning removes it, overriding writes it, comparing
245        /// reopens it).
246        temp: PathBuf,
247        /// The newer on-disk file, shown so the user can open it by hand.
248        disk_path: PathBuf,
249    },
250    /// Create a relationship from the detail view (#1359), in two steps.
251    ///
252    /// `Step::Target` asks for the other issue as a **selector** — the same string
253    /// `tix link <from> <to>` takes, so `#4`, `4`, or a title fragment all work and
254    /// an unresolvable one produces the CLI's own error rather than a TUI-specific
255    /// one. `Step::Kind` then asks for the kind, and it is **required**: C-001 forbids
256    /// an untypeable relationship, so there is no default to fall back to and an
257    /// empty or unrecognised answer is a refusal, not a guess.
258    LinkCreate {
259        buffer: String,
260        step: LinkStep,
261        /// The target resolved by the `Target` step, carried forward to `Kind`.
262        ///
263        /// Carried rather than re-resolved: the first step already applied the CLI's
264        /// selector rules and reported its errors, and re-reading the buffer's
265        /// meaning a second time would be a second place for the two to disagree. A
266        /// `Kind` step with no target cannot happen — the mode is only entered in
267        /// that state with one.
268        target: Option<u32>,
269    },
270    /// Re-kind or retarget the relationship under the detail pane's cursor.
271    ///
272    /// Carries the target it was opened with so the kind step can submit an edit
273    /// rather than a create: `tix link edit <id>` is an edit precisely because the
274    /// stable id survives a re-kind, and a create-then-remove pair would lose it.
275    LinkEdit {
276        index: usize,
277        buffer: String,
278        step: LinkStep,
279        /// The new target, when the user supplied one. `None` means **keep the
280        /// existing target** and change only the kind — the same as `tix link edit
281        /// <id> --type …` with no `--to`.
282        target: Option<u32>,
283        /// The new kind, when the user supplied one. `None` likewise means keep.
284        kind: Option<crate::relationships::RelationKind>,
285    },
286    /// Confirm removing the relationship under the cursor.
287    ///
288    /// A relationship is stored in exactly one issue's file, so removing it is a
289    /// write to another issue from the one on screen. It gets a confirmation for the
290    /// same reason `ConfirmDelete` does, and the confirmation names the edge.
291    ConfirmRemoveRelationship {
292        index: usize,
293    },
294    /// Set, change or clear the open issue's parent (tix #1384).
295    ///
296    /// The detail pane's `P` opens it. Enter submits the typed selector and Esc
297    /// cancels; an **empty** line clears the parent. The write goes through
298    /// `Store::set_parent`/`Store::clear_parent`, the same hierarchy seam
299    /// `tix edit --parent` uses, so one accepted change appends exactly one
300    /// `parent_set`/`parent_changed`/`parent_cleared` History event — the TUI
301    /// half of the CLI/TUI parity the create-only form could not express.
302    ParentPrompt {
303        buffer: String,
304    },
305}