1use crate::app::App;
7use crate::event::Intent;
8use crate::focus::{FocusTarget, current_focus};
9use crate::modal::{KeyPolicy, Modal};
10use crate::pane::PaneId;
11use crate::view::Tab;
12use crate::widgets::{EditMode, VimAction, VimMode, WidgetKind};
13use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
14
15#[must_use]
16pub fn translate(app: &App, key: KeyEvent) -> Option<Intent> {
17 match current_focus(app) {
18 FocusTarget::CmdLine => {
19 if app.cmdline.editor.mode() == EditMode::Vim
20 && app.cmdline.editor.vim_mode() == VimMode::Normal
21 {
22 translate_vim_normal(key)
23 } else {
24 translate_cmdline_insert(key)
25 }
26 }
27 FocusTarget::ConsoleInput => translate_console_insert(app.console.panes.focused(), key),
28 FocusTarget::Overlay => translate_modal(app.overlays.top(), key),
29 FocusTarget::ViewPane => translate_tab(app, key),
30 }
31}
32
33fn translate_tab(app: &App, key: KeyEvent) -> Option<Intent> {
34 match key.code {
35 KeyCode::Char('q') => Some(Intent::Quit),
36 KeyCode::Char(':') => Some(Intent::OpenCommandLine),
37 KeyCode::Char('?') => Some(Intent::OpenHelp),
38 KeyCode::Tab => Some(Intent::NextTab),
39 KeyCode::BackTab => Some(Intent::PreviousTab),
40 KeyCode::Char('1') => Some(Intent::SelectTab(Tab::Accounts)),
41 KeyCode::Char('2') => Some(Intent::SelectTab(Tab::Transactions)),
42 KeyCode::Char('3') => Some(Intent::SelectTab(Tab::Commodities)),
43 KeyCode::Char('4') => Some(Intent::SelectTab(Tab::Reports)),
44 KeyCode::Char('5') => Some(Intent::SelectTab(Tab::Config)),
45 KeyCode::Char('6') => Some(Intent::SelectTab(Tab::Console)),
46 KeyCode::Char('v') if key.modifiers.contains(KeyModifiers::CONTROL) => {
47 Some(Intent::ToggleEditMode)
48 }
49 KeyCode::Char('e') | KeyCode::Enter if app.active_tab == Tab::Config => {
50 Some(Intent::EditConfig)
51 }
52 KeyCode::Down | KeyCode::Char('j') if app.active_tab == Tab::Config => {
53 Some(Intent::ListSelectNext)
54 }
55 KeyCode::Up | KeyCode::Char('k') if app.active_tab == Tab::Config => {
56 Some(Intent::ListSelectPrev)
57 }
58 KeyCode::Char('r') if app.active_tab == Tab::Config => Some(Intent::RefreshTab),
59 KeyCode::Down | KeyCode::Char('j')
60 if matches!(
61 app.active_tab,
62 Tab::Accounts | Tab::Transactions | Tab::Commodities
63 ) =>
64 {
65 Some(Intent::ListSelectNext)
66 }
67 KeyCode::Up | KeyCode::Char('k')
68 if matches!(
69 app.active_tab,
70 Tab::Accounts | Tab::Transactions | Tab::Commodities
71 ) =>
72 {
73 Some(Intent::ListSelectPrev)
74 }
75 KeyCode::Char('r')
76 if matches!(
77 app.active_tab,
78 Tab::Accounts | Tab::Transactions | Tab::Commodities
79 ) =>
80 {
81 Some(Intent::RefreshTab)
82 }
83 KeyCode::Char('d') | KeyCode::Delete if app.active_tab == Tab::Transactions => {
84 Some(Intent::ListDelete)
85 }
86 KeyCode::Char('e') if matches!(app.active_tab, Tab::Accounts | Tab::Transactions) => {
87 Some(Intent::ListEdit)
88 }
89 KeyCode::Char('i') | KeyCode::Enter if app.active_tab == Tab::Console => {
90 Some(Intent::ConsoleFocus)
91 }
92 _ => None,
93 }
94}
95
96fn translate_console_insert(pane: PaneId, key: KeyEvent) -> Option<Intent> {
99 if matches!(key.code, KeyCode::Tab | KeyCode::BackTab) {
101 return Some(Intent::ConsoleCyclePane);
102 }
103 match pane {
104 PaneId::Prompt => translate_prompt_key(key),
105 PaneId::Scrollback => translate_scrollback_key(key),
106 }
107}
108
109fn translate_prompt_key(key: KeyEvent) -> Option<Intent> {
114 match key.code {
115 KeyCode::Esc => Some(Intent::ConsoleBlur),
116 KeyCode::Enter => Some(Intent::ConsoleSubmit),
117 KeyCode::Up => Some(Intent::ConsoleHistoryPrev),
118 KeyCode::Down => Some(Intent::ConsoleHistoryNext),
119 KeyCode::Backspace => Some(Intent::DeleteBackward),
120 KeyCode::Left => Some(Intent::MoveLeft),
121 KeyCode::Right => Some(Intent::MoveRight),
122 KeyCode::Home => Some(Intent::MoveHome),
123 KeyCode::End => Some(Intent::MoveEnd),
124 KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
125 Some(Intent::ConsoleInterrupt)
126 }
127 KeyCode::Char('a') if key.modifiers.contains(KeyModifiers::CONTROL) => {
128 Some(Intent::MoveHome)
129 }
130 KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => {
131 Some(Intent::MoveEnd)
132 }
133 KeyCode::Char('k') if key.modifiers.contains(KeyModifiers::CONTROL) => {
134 Some(Intent::KillToEnd)
135 }
136 KeyCode::Char('w') if key.modifiers.contains(KeyModifiers::CONTROL) => {
137 Some(Intent::KillWordBackward)
138 }
139 KeyCode::Char(c) => Some(Intent::InsertChar(c)),
140 _ => None,
141 }
142}
143
144fn translate_scrollback_key(key: KeyEvent) -> Option<Intent> {
145 match key.code {
146 KeyCode::Esc => Some(Intent::ConsoleBlur),
147 KeyCode::Up => Some(Intent::ScrollLineUp),
148 KeyCode::Down => Some(Intent::ScrollLineDown),
149 KeyCode::PageUp => Some(Intent::ScrollPageUp),
150 KeyCode::PageDown => Some(Intent::ScrollPageDown),
151 KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
152 Some(Intent::ConsoleInterrupt)
153 }
154 _ => None,
155 }
156}
157
158fn translate_modal(modal: Option<&Modal>, key: KeyEvent) -> Option<Intent> {
159 if matches!(modal, Some(Modal::Confirm { .. })) {
161 return match key.code {
162 KeyCode::Char('y') | KeyCode::Enter => Some(Intent::ConfirmYes),
163 KeyCode::Char('n') | KeyCode::Char('q') | KeyCode::Esc => Some(Intent::CloseTopmost),
164 _ => None,
165 };
166 }
167 let policy = modal.map_or(KeyPolicy::Display, Modal::key_policy);
172 let focused_kind = focused_widget_kind(modal);
173 let splits_sub = focused_splits_sub_kind(modal);
174 let splits_boundary = focused_splits_boundary(modal);
175 match key.code {
176 KeyCode::Esc => Some(Intent::CloseTopmost),
177 KeyCode::Char('q') if policy == KeyPolicy::Display => Some(Intent::CloseTopmost),
178 KeyCode::Tab if focused_kind == Some(WidgetKind::Splits) => match splits_boundary {
181 Some((_, true)) => Some(Intent::NextTab),
182 _ => Some(Intent::ColNext),
183 },
184 KeyCode::BackTab if focused_kind == Some(WidgetKind::Splits) => match splits_boundary {
185 Some((true, _)) => Some(Intent::PreviousTab),
186 _ => Some(Intent::ColPrev),
187 },
188 KeyCode::Up
190 if focused_kind == Some(WidgetKind::Splits)
191 && splits_sub == Some(WidgetKind::Select) =>
192 {
193 Some(Intent::SelectPrev)
194 }
195 KeyCode::Down
196 if focused_kind == Some(WidgetKind::Splits)
197 && splits_sub == Some(WidgetKind::Select) =>
198 {
199 Some(Intent::SelectNext)
200 }
201 KeyCode::Up if focused_kind == Some(WidgetKind::Splits) => Some(Intent::RowPrev),
202 KeyCode::Down if focused_kind == Some(WidgetKind::Splits) => Some(Intent::RowNext),
203 KeyCode::Char('+') if focused_kind == Some(WidgetKind::Splits) => Some(Intent::SplitAddRow),
207 KeyCode::Char('n')
208 if focused_kind == Some(WidgetKind::Splits)
209 && key.modifiers.contains(KeyModifiers::CONTROL) =>
210 {
211 Some(Intent::SplitAddRow)
212 }
213 KeyCode::Char('-') if focused_kind == Some(WidgetKind::Splits) => {
214 Some(Intent::SplitRemoveRow)
215 }
216 KeyCode::Char('d')
217 if focused_kind == Some(WidgetKind::Splits)
218 && key.modifiers.contains(KeyModifiers::CONTROL) =>
219 {
220 Some(Intent::SplitRemoveRow)
221 }
222 KeyCode::Up if focused_kind == Some(WidgetKind::Date) => Some(Intent::DateStepBack),
224 KeyCode::Down if focused_kind == Some(WidgetKind::Date) => Some(Intent::DateStepForward),
225 KeyCode::Up if focused_kind == Some(WidgetKind::Select) => Some(Intent::SelectPrev),
227 KeyCode::Down if focused_kind == Some(WidgetKind::Select) => Some(Intent::SelectNext),
228 KeyCode::Enter if focused_kind == Some(WidgetKind::Select) => Some(Intent::SelectConfirm),
229 KeyCode::Enter => Some(Intent::SubmitCommandLine),
230 KeyCode::Tab => Some(Intent::NextTab),
231 KeyCode::BackTab => Some(Intent::PreviousTab),
232 KeyCode::Backspace => Some(Intent::DeleteBackward),
233 KeyCode::Char(c) => Some(Intent::InsertChar(c)),
234 _ => None,
235 }
236}
237
238fn focused_widget_kind(modal: Option<&Modal>) -> Option<WidgetKind> {
240 match modal? {
241 Modal::Form(form) => form.focused_widget_kind(),
242 Modal::Help | Modal::Confirm { .. } => None,
243 }
244}
245
246fn focused_splits_sub_kind(modal: Option<&Modal>) -> Option<WidgetKind> {
248 match modal? {
249 Modal::Form(form) => form.focused_splits_sub_kind(),
250 Modal::Help | Modal::Confirm { .. } => None,
251 }
252}
253
254fn focused_splits_boundary(modal: Option<&Modal>) -> Option<(bool, bool)> {
256 match modal? {
257 Modal::Form(form) => form.focused_splits_boundary(),
258 Modal::Help | Modal::Confirm { .. } => None,
259 }
260}
261
262fn translate_cmdline_insert(key: KeyEvent) -> Option<Intent> {
263 match key.code {
264 KeyCode::Esc => Some(Intent::CloseTopmost),
265 KeyCode::Enter => Some(Intent::SubmitCommandLine),
266 KeyCode::Tab => Some(Intent::CompleteCommandLine),
267 KeyCode::Backspace => Some(Intent::DeleteBackward),
268 KeyCode::Left => Some(Intent::MoveLeft),
269 KeyCode::Right => Some(Intent::MoveRight),
270 KeyCode::Home => Some(Intent::MoveHome),
271 KeyCode::End => Some(Intent::MoveEnd),
272 KeyCode::Char('a') if key.modifiers.contains(KeyModifiers::CONTROL) => {
273 Some(Intent::MoveHome)
274 }
275 KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => {
276 Some(Intent::MoveEnd)
277 }
278 KeyCode::Char('k') if key.modifiers.contains(KeyModifiers::CONTROL) => {
279 Some(Intent::KillToEnd)
280 }
281 KeyCode::Char('w') if key.modifiers.contains(KeyModifiers::CONTROL) => {
282 Some(Intent::KillWordBackward)
283 }
284 KeyCode::Char(c) => Some(Intent::InsertChar(c)),
285 _ => None,
286 }
287}
288
289fn translate_vim_normal(key: KeyEvent) -> Option<Intent> {
290 match key.code {
291 KeyCode::Esc => Some(Intent::CloseTopmost),
292 KeyCode::Enter => Some(Intent::SubmitCommandLine),
293 KeyCode::Char('h') => Some(Intent::Vim(VimAction::MoveLeft)),
294 KeyCode::Char('l') => Some(Intent::Vim(VimAction::MoveRight)),
295 KeyCode::Char('0') => Some(Intent::Vim(VimAction::MoveHome)),
296 KeyCode::Char('$') => Some(Intent::Vim(VimAction::MoveEnd)),
297 KeyCode::Char('w') => Some(Intent::Vim(VimAction::WordForward)),
298 KeyCode::Char('b') => Some(Intent::Vim(VimAction::WordBackward)),
299 KeyCode::Char('x') => Some(Intent::Vim(VimAction::DeleteChar)),
300 KeyCode::Char('i') => Some(Intent::Vim(VimAction::InsertAtCursor)),
301 KeyCode::Char('a') => Some(Intent::Vim(VimAction::InsertAfterCursor)),
302 KeyCode::Char('I') => Some(Intent::Vim(VimAction::InsertAtLineStart)),
303 KeyCode::Char('A') => Some(Intent::Vim(VimAction::InsertAtLineEnd)),
304 KeyCode::Char('D') => Some(Intent::Vim(VimAction::DeleteWordForward)),
306 KeyCode::Char('B') => Some(Intent::Vim(VimAction::DeleteWordBackward)),
307 _ => None,
308 }
309}
310
311#[cfg(test)]
312mod tests;