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}
78
79impl DetailTab {
80    /// Tab order cycled by Enter in the detail pane.
81    pub const ALL: [DetailTab; 4] = [
82        DetailTab::Show,
83        DetailTab::Excerpt,
84        DetailTab::Tree,
85        DetailTab::Related,
86    ];
87
88    /// Tab label drawn on the detail border.
89    pub fn title(self) -> &'static str {
90        match self {
91            Self::Show => "show",
92            Self::Excerpt => "excerpt",
93            Self::Tree => "tree",
94            Self::Related => "related",
95        }
96    }
97
98    /// Next tab in cycle order.
99    pub fn next(self) -> Self {
100        let i = Self::ALL.iter().position(|t| *t == self).unwrap_or(0);
101        Self::ALL[(i + 1) % Self::ALL.len()]
102    }
103}
104
105/// Which pane receives movement keys.
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub enum Focus {
108    /// Row list on the left.
109    Rows,
110    /// Detail pane on the right.
111    Detail,
112}
113
114/// Line prompt opened by `/`, `n`, or `p`.
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub enum PromptKind {
117    /// Search query for the Search pane.
118    Search,
119    /// Logbook note on the selected issue.
120    Note,
121    /// Project filter. Empty clears it.
122    Project,
123}
124
125/// Destructive state change waiting for `y`.
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub enum ConfirmKind {
128    /// Set state to DONE.
129    Done,
130    /// Set state to CANCELLED.
131    Cancelled,
132}
133
134impl ConfirmKind {
135    /// Org TODO keyword this confirmation applies.
136    pub fn state(self) -> &'static str {
137        match self {
138            Self::Done => "DONE",
139            Self::Cancelled => "CANCELLED",
140        }
141    }
142}
143
144/// True for Press and Repeat; false for Release.
145pub fn is_press(key: KeyEvent) -> bool {
146    key.kind == KeyEventKind::Press || key.kind == KeyEventKind::Repeat
147}
148
149/// Printable character from `key`, including Shift. Other modifiers drop it.
150pub fn char_of(key: KeyEvent) -> Option<char> {
151    match key.code {
152        KeyCode::Char(c) if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT => {
153            Some(c)
154        }
155        _ => None,
156    }
157}
158
159/// Overlay text shown on `?`.
160pub const HELP: &str = "\
161vissue tui
162
163j/k, arrows   move
164Tab, 1-5      pane (Ready List Claims Agenda Search)
165Enter         focus detail / cycle detail tab
166p             project filter
167/             search
168c             claim
169n             note
170s             cycle TODO / STARTED / BLOCKED
171D             DONE (confirm)
172X             CANCELLED (confirm)
173o             open (shared selection)
174y             copy id
175R             reload
176?             this help
177q / Esc       quit / back
178
179Body edits stay in the file.
180body lives in file; open the range above
181";