Skip to main content

vissue_tui/
keys.rs

1//! Key dispatch. Bindings are listed on `?`.
2
3use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
4
5/// What the event loop does after a key.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum Action {
8    /// Stay in the event loop.
9    Continue,
10    /// Leave the event loop.
11    Quit,
12}
13
14/// One of the five list surfaces.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Pane {
17    /// Actionable ready queue.
18    Ready,
19    /// Full filtered list.
20    List,
21    /// Open claims.
22    Claims,
23    /// Deadlines and scheduled dates.
24    Agenda,
25    /// Title and body search.
26    Search,
27}
28
29impl Pane {
30    /// Tab order, left to right.
31    pub const ALL: [Pane; 5] = [
32        Pane::Ready,
33        Pane::List,
34        Pane::Claims,
35        Pane::Agenda,
36        Pane::Search,
37    ];
38
39    /// Tab label drawn on the board.
40    pub fn title(self) -> &'static str {
41        match self {
42            Self::Ready => "Ready",
43            Self::List => "List",
44            Self::Claims => "Claims",
45            Self::Agenda => "Agenda",
46            Self::Search => "Search",
47        }
48    }
49
50    /// Index into [`Self::ALL`].
51    pub fn index(self) -> usize {
52        Self::ALL.iter().position(|p| *p == self).unwrap_or(0)
53    }
54
55    /// Pane at `i` modulo the tab count.
56    pub fn from_index(i: usize) -> Self {
57        Self::ALL[i % Self::ALL.len()]
58    }
59
60    /// Next pane in tab order.
61    pub fn next(self) -> Self {
62        Self::from_index(self.index() + 1)
63    }
64}
65
66/// Right-hand detail surface.
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum DetailTab {
69    /// Metadata from `issue/get`.
70    Show,
71    /// On-disk heading range.
72    Excerpt,
73    /// Parent and child tree.
74    Tree,
75    /// Related-issue hits.
76    Related,
77    /// The working set: plan, declared inputs, and what they produced.
78    Recall,
79}
80
81impl DetailTab {
82    /// Tab order cycled by Enter in the detail pane.
83    pub const ALL: [DetailTab; 5] = [
84        DetailTab::Show,
85        DetailTab::Excerpt,
86        DetailTab::Tree,
87        DetailTab::Related,
88        DetailTab::Recall,
89    ];
90
91    /// Tab label drawn on the detail border.
92    pub fn title(self) -> &'static str {
93        match self {
94            Self::Show => "show",
95            Self::Excerpt => "excerpt",
96            Self::Tree => "tree",
97            Self::Related => "related",
98            Self::Recall => "recall",
99        }
100    }
101
102    /// Next tab in cycle order.
103    pub fn next(self) -> Self {
104        let i = Self::ALL.iter().position(|t| *t == self).unwrap_or(0);
105        Self::ALL[(i + 1) % Self::ALL.len()]
106    }
107}
108
109/// Which pane receives movement keys.
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum Focus {
112    /// Row list on the left.
113    Rows,
114    /// Detail pane on the right.
115    Detail,
116}
117
118/// Line prompt opened by `/`, `n`, or `p`.
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub enum PromptKind {
121    /// Search query for the Search pane.
122    Search,
123    /// Logbook note on the selected issue.
124    Note,
125    /// Deed accession to cite on the selected issue.
126    Deed,
127    /// Project filter. Empty clears it.
128    Project,
129}
130
131/// Destructive state change waiting for `y`.
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub enum ConfirmKind {
134    /// Set state to DONE.
135    Done,
136    /// Set state to CANCELLED.
137    Cancelled,
138}
139
140impl ConfirmKind {
141    /// Org TODO keyword this confirmation applies.
142    pub fn state(self) -> &'static str {
143        match self {
144            Self::Done => "DONE",
145            Self::Cancelled => "CANCELLED",
146        }
147    }
148}
149
150/// True for Press and Repeat; false for Release.
151pub fn is_press(key: KeyEvent) -> bool {
152    key.kind == KeyEventKind::Press || key.kind == KeyEventKind::Repeat
153}
154
155/// Printable character from `key`, including Shift. Other modifiers drop it.
156pub fn char_of(key: KeyEvent) -> Option<char> {
157    match key.code {
158        KeyCode::Char(c) if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT => {
159            Some(c)
160        }
161        _ => None,
162    }
163}
164
165/// Overlay text shown on `?`.
166pub const HELP: &str = "\
167vissue tui
168
169j/k, arrows   move
170Tab, 1-5      pane (Ready List Claims Agenda Search)
171Enter         focus detail / cycle detail tab
172p             project filter
173/             search
174c             claim
175n             note
176s             cycle TODO / STARTED / BLOCKED
177D             DONE (confirm)
178X             CANCELLED (confirm)
179o             open (shared selection)
180y             copy id
181R             reload
182?             this help
183q / Esc       quit / back
184
185Body edits stay in the file.
186body lives in file; open the range above
187";