Skip to main content

trem_tui/
input.rs

1//! Keyboard routing for **modal editors**: pattern grid and signal graph. Each editor
2//! owns a key family; [`InputContext`] disambiguates global chords (Tab, `?`, Esc) vs
3//! nested-graph exit. Future editors: see repository `docs/tui-editor-roadmap.md`.
4//!
5//! Full bindings: **`?`** help overlay. Sidebar shows a short **popular** subset only.
6
7use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
8
9/// Which **editor surface** is focused (modal). Tab cycles this list.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum Editor {
12    /// Step sequencer: time × voice.
13    Pattern,
14    /// Nested audio graph + parameters.
15    Graph,
16}
17
18impl Editor {
19    /// Short transport tab label.
20    pub fn tab_label(self) -> &'static str {
21        match self {
22            Editor::Pattern => "SEQ",
23            Editor::Graph => "GRAPH",
24        }
25    }
26
27    /// Sidebar / docs: human name.
28    pub fn title(self) -> &'static str {
29        match self {
30            Editor::Pattern => "Sequencer",
31            Editor::Graph => "Graph",
32        }
33    }
34
35    /// One-line intent for the modal system.
36    pub fn intent(self) -> &'static str {
37        match self {
38            Editor::Pattern => "time · voices",
39            Editor::Graph => "signal · routing",
40        }
41    }
42
43    pub fn next(self) -> Self {
44        match self {
45            Editor::Pattern => Editor::Graph,
46            Editor::Graph => Editor::Pattern,
47        }
48    }
49
50    pub const ALL: [Editor; 2] = [Editor::Pattern, Editor::Graph];
51}
52
53/// State needed to route keys without ambiguity.
54#[derive(Debug, Clone, Copy)]
55pub struct InputContext<'a> {
56    pub editor: Editor,
57    pub mode: &'a Mode,
58    /// True when graph editor is inside a nested graph (`graph_path` non-empty).
59    pub graph_is_nested: bool,
60    pub help_open: bool,
61}
62
63/// Bottom pane visualizer: stereo waveform or frequency spectrum.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum BottomPane {
66    Waveform,
67    Spectrum,
68}
69
70impl BottomPane {
71    pub fn next(self) -> Self {
72        match self {
73            BottomPane::Waveform => BottomPane::Spectrum,
74            BottomPane::Spectrum => BottomPane::Waveform,
75        }
76    }
77
78    pub fn label(self) -> &'static str {
79        match self {
80            BottomPane::Waveform => "SCOPE",
81            BottomPane::Spectrum => "SPECTRUM",
82        }
83    }
84}
85
86/// Within an editor: navigate vs change values / paint notes.
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum Mode {
89    Normal,
90    Edit,
91}
92
93impl Mode {
94    pub fn label(self) -> &'static str {
95        match self {
96            Mode::Normal => "NAV",
97            Mode::Edit => "EDIT",
98        }
99    }
100}
101
102/// Semantic user intent from one key (may be ignored in [`crate::App::handle_action`]).
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum Action {
105    Quit,
106    /// Tab: next editor.
107    CycleEditor,
108    /// `e` / Esc: toggle edit mode (when not in help / graph-exit).
109    ToggleEdit,
110    TogglePlay,
111    MoveUp,
112    MoveDown,
113    MoveLeft,
114    MoveRight,
115    NoteInput(i32),
116    DeleteNote,
117    OctaveUp,
118    OctaveDown,
119    BpmUp,
120    BpmDown,
121    ParamFineUp,
122    ParamFineDown,
123    EuclideanFill,
124    RandomizeVoice,
125    ReverseVoice,
126    ShiftVoiceLeft,
127    ShiftVoiceRight,
128    VelocityUp,
129    VelocityDown,
130    GateCycle,
131    Undo,
132    Redo,
133    SwingUp,
134    SwingDown,
135    SaveProject,
136    LoadProject,
137    CycleBottomPane,
138    EnterGraph,
139    ExitGraph,
140    /// `?` toggles full key reference overlay.
141    ToggleHelp,
142}
143
144/// Maps a key to an action; release events and unbound keys yield `None`.
145pub fn handle_key(key: KeyEvent, ctx: &InputContext<'_>) -> Option<Action> {
146    if key.kind == KeyEventKind::Release {
147        return None;
148    }
149
150    if ctx.help_open {
151        return match key.code {
152            KeyCode::Esc | KeyCode::Char('?') => Some(Action::ToggleHelp),
153            _ => None,
154        };
155    }
156
157    if key.modifiers.contains(KeyModifiers::CONTROL) {
158        return match key.code {
159            KeyCode::Char('c') | KeyCode::Char('q') => Some(Action::Quit),
160            KeyCode::Char('s') => Some(Action::SaveProject),
161            KeyCode::Char('o') => Some(Action::LoadProject),
162            KeyCode::Char('z') => Some(Action::Undo),
163            KeyCode::Char('y') => Some(Action::Redo),
164            _ => None,
165        };
166    }
167
168    if key.modifiers.contains(KeyModifiers::SHIFT) {
169        match key.code {
170            KeyCode::Left => return Some(Action::ParamFineDown),
171            KeyCode::Right => return Some(Action::ParamFineUp),
172            KeyCode::Char('U') => return Some(Action::Redo),
173            _ => {}
174        }
175    }
176
177    match key.code {
178        KeyCode::Tab => return Some(Action::CycleEditor),
179        KeyCode::Char('?') => return Some(Action::ToggleHelp),
180        KeyCode::Char(' ') => return Some(Action::TogglePlay),
181        KeyCode::Up => return Some(Action::MoveUp),
182        KeyCode::Down => return Some(Action::MoveDown),
183        KeyCode::Left => return Some(Action::MoveLeft),
184        KeyCode::Right => return Some(Action::MoveRight),
185        KeyCode::Char('+') | KeyCode::Char('=') => return Some(Action::BpmUp),
186        KeyCode::Char('-') => return Some(Action::BpmDown),
187        KeyCode::Char('[') => return Some(Action::OctaveDown),
188        KeyCode::Char(']') => return Some(Action::OctaveUp),
189        KeyCode::Char('{') => return Some(Action::SwingDown),
190        KeyCode::Char('}') => return Some(Action::SwingUp),
191        KeyCode::Char('`') => return Some(Action::CycleBottomPane),
192        KeyCode::Esc if *ctx.mode == Mode::Edit => return Some(Action::ToggleEdit),
193        KeyCode::Esc
194            if *ctx.mode == Mode::Normal && ctx.editor == Editor::Graph && ctx.graph_is_nested =>
195        {
196            return Some(Action::ExitGraph);
197        }
198        _ => {}
199    }
200
201    match ctx.editor {
202        Editor::Pattern => pattern_keys(key.code, ctx.mode),
203        Editor::Graph => graph_keys(key.code, ctx.mode),
204    }
205}
206
207fn pattern_keys(code: KeyCode, mode: &Mode) -> Option<Action> {
208    match mode {
209        Mode::Normal => match code {
210            KeyCode::Char('q') => Some(Action::Quit),
211            KeyCode::Char('e') => Some(Action::ToggleEdit),
212            KeyCode::Char('u') => Some(Action::Undo),
213            KeyCode::Char('h') => Some(Action::MoveLeft),
214            KeyCode::Char('l') => Some(Action::MoveRight),
215            KeyCode::Char('k') => Some(Action::MoveUp),
216            KeyCode::Char('j') => Some(Action::MoveDown),
217            _ => None,
218        },
219        Mode::Edit => match code {
220            KeyCode::Delete | KeyCode::Backspace => Some(Action::DeleteNote),
221            KeyCode::Char('z') => Some(Action::NoteInput(0)),
222            KeyCode::Char('s') => Some(Action::NoteInput(1)),
223            KeyCode::Char('x') => Some(Action::NoteInput(2)),
224            KeyCode::Char('d') => Some(Action::NoteInput(3)),
225            KeyCode::Char('c') => Some(Action::NoteInput(4)),
226            KeyCode::Char('v') => Some(Action::NoteInput(5)),
227            KeyCode::Char('g') => Some(Action::NoteInput(6)),
228            KeyCode::Char('b') => Some(Action::NoteInput(7)),
229            KeyCode::Char('h') => Some(Action::NoteInput(8)),
230            KeyCode::Char('n') => Some(Action::NoteInput(9)),
231            KeyCode::Char('j') => Some(Action::NoteInput(10)),
232            KeyCode::Char('m') => Some(Action::NoteInput(11)),
233            KeyCode::Char(ch @ '0'..='9') => Some(Action::NoteInput(ch as i32 - '0' as i32)),
234            KeyCode::Char('f') => Some(Action::EuclideanFill),
235            KeyCode::Char('r') => Some(Action::RandomizeVoice),
236            KeyCode::Char('t') => Some(Action::ReverseVoice),
237            KeyCode::Char(',') => Some(Action::ShiftVoiceLeft),
238            KeyCode::Char('.') => Some(Action::ShiftVoiceRight),
239            KeyCode::Char('w') => Some(Action::VelocityUp),
240            KeyCode::Char('q') => Some(Action::VelocityDown),
241            KeyCode::Char('a') => Some(Action::GateCycle),
242            _ => None,
243        },
244    }
245}
246
247fn graph_keys(code: KeyCode, mode: &Mode) -> Option<Action> {
248    match mode {
249        Mode::Normal => match code {
250            KeyCode::Char('q') => Some(Action::Quit),
251            KeyCode::Char('e') => Some(Action::ToggleEdit),
252            KeyCode::Char('u') => Some(Action::Undo),
253            KeyCode::Char('h') => Some(Action::MoveLeft),
254            KeyCode::Char('l') => Some(Action::MoveRight),
255            KeyCode::Char('k') => Some(Action::MoveUp),
256            KeyCode::Char('j') => Some(Action::MoveDown),
257            KeyCode::Enter => Some(Action::EnterGraph),
258            _ => None,
259        },
260        Mode::Edit => None,
261    }
262}