1use crate::error::{GwmError, Result};
64use crate::tui::keymap::{KeyStroke, Source};
65use crossterm::event::{KeyCode, KeyModifiers};
66use std::collections::HashMap;
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
77pub enum KeyContext {
78 Create,
80 Confirm,
82 Help,
84 CommandLogs,
86 Config,
88 ConfigEdit,
92 Report,
94 OpenMenu,
96 CommandPalette,
98 LinkChooseTarget,
100 LinkInputNumber,
102 ExecPicker,
104 Clean,
106}
107
108impl KeyContext {
109 pub fn config_path(self) -> &'static str {
111 match self {
112 KeyContext::Create => "create",
113 KeyContext::Confirm => "confirm",
114 KeyContext::Help => "help",
115 KeyContext::CommandLogs => "command_logs",
116 KeyContext::Config => "config",
117 KeyContext::ConfigEdit => "config.edit",
118 KeyContext::Report => "report",
119 KeyContext::OpenMenu => "open_menu",
120 KeyContext::CommandPalette => "palette",
121 KeyContext::LinkChooseTarget => "link.choose_target",
122 KeyContext::LinkInputNumber => "link.input_number",
123 KeyContext::ExecPicker => "exec",
124 KeyContext::Clean => "clean",
125 }
126 }
127
128 pub fn from_config_path(path: &str) -> Option<Self> {
131 Self::all().iter().copied().find(|c| c.config_path() == path)
132 }
133
134 pub fn all() -> &'static [KeyContext] {
136 use KeyContext::*;
137 &[
138 Create,
139 Confirm,
140 Help,
141 CommandLogs,
142 Config,
143 ConfigEdit,
144 Report,
145 OpenMenu,
146 CommandPalette,
147 LinkChooseTarget,
148 LinkInputNumber,
149 ExecPicker,
150 Clean,
151 ]
152 }
153}
154
155macro_rules! define_modal_actions {
163 ( $( $ctx:ident { $( $variant:ident => $verb:literal [ $( $chord:literal ),* $(,)? ] ),* $(,)? } )* ) => {
164 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
168 pub enum ModalAction {
169 $( $( $variant, )* )*
170 }
171
172 impl ModalAction {
173 pub fn context(self) -> KeyContext {
175 match self { $( $( ModalAction::$variant => KeyContext::$ctx, )* )* }
176 }
177
178 pub fn verb(self) -> &'static str {
180 match self { $( $( ModalAction::$variant => $verb, )* )* }
181 }
182
183 fn default_chord_strs(self) -> &'static [&'static str] {
185 match self { $( $( ModalAction::$variant => &[ $( $chord, )* ], )* )* }
186 }
187
188 pub fn all() -> impl Iterator<Item = Self> {
190 [ $( $( ModalAction::$variant, )* )* ].into_iter()
191 }
192 }
193 };
194}
195
196define_modal_actions! {
197 Create {
198 CreateCancel => "cancel" [ "Esc" ],
199 CreateNextField => "next_field" [ "Tab" ],
200 CreatePrevField => "prev_field" [ "BackTab" ],
201 CreateSubmit => "submit" [ "Enter" ],
202 CreatePrevType => "prev_type" [ "Up", "Left", "h" ],
203 CreateNextType => "next_type" [ "Down", "Right", "l" ],
204 }
205 Confirm {
206 ConfirmConfirm => "confirm" [ "y" ],
207 ConfirmActivate => "activate" [ "Enter" ],
208 ConfirmCancel => "cancel" [ "n", "Esc" ],
209 ConfirmFocusConfirm => "focus_confirm" [ "Left", "h" ],
210 ConfirmFocusCancel => "focus_cancel" [ "Right", "l" ],
211 ConfirmToggleFocus => "toggle_focus" [ "Tab" ],
212 }
213 Help {
214 HelpClose => "close" [ "Esc", "q", "?" ],
215 HelpScrollDown => "scroll_down" [ "Down", "j" ],
216 HelpScrollUp => "scroll_up" [ "Up", "k" ],
217 HelpScrollRight => "scroll_right" [ "Right", "l" ],
218 HelpScrollLeft => "scroll_left" [ "Left", "h" ],
219 HelpScrollTop => "scroll_top" [ "Home", "g" ],
220 HelpScrollBottom => "scroll_bottom" [ "End", "G" ],
221 }
222 CommandLogs {
223 CommandLogsClose => "close" [ "Esc", "q" ],
224 CommandLogsCopy => "copy" [ "y" ],
225 CommandLogsScrollDown => "scroll_down" [ "Down", "j" ],
226 CommandLogsScrollUp => "scroll_up" [ "Up", "k" ],
227 CommandLogsScrollRight => "scroll_right" [ "Right", "l" ],
228 CommandLogsScrollLeft => "scroll_left" [ "Left", "h" ],
229 CommandLogsScrollTop => "scroll_top" [ "Home", "g" ],
230 CommandLogsScrollBottom => "scroll_bottom" [ "End", "G" ],
231 }
232 Config {
233 ConfigClose => "close" [ "Esc", "q" ],
234 ConfigNextTab => "next_tab" [ "Tab" ],
235 ConfigPrevTab => "prev_tab" [ "BackTab" ],
236 ConfigToggleLayer => "toggle_layer" [ "L" ],
237 ConfigActivate => "activate" [ "Space", "Enter" ],
238 ConfigSelectNext => "select_next" [ "Down", "j" ],
239 ConfigSelectPrev => "select_prev" [ "Up", "k" ],
240 ConfigScrollRight => "scroll_right" [ "Right", "l" ],
241 ConfigScrollLeft => "scroll_left" [ "Left", "h" ],
242 ConfigScrollTop => "scroll_top" [ "Home", "g" ],
243 ConfigScrollBottom => "scroll_bottom" [ "End", "G" ],
244 }
245 ConfigEdit {
246 ConfigEditSubmit => "submit" [ "Enter" ],
247 ConfigEditCancel => "cancel" [ "Esc" ],
248 }
249 Report {
250 ReportClose => "close" [ "Esc", "q", "Enter" ],
251 }
252 OpenMenu {
253 OpenMenuClose => "close" [ "Esc", "q" ],
254 OpenMenuToggle => "toggle" [ "j", "k", "Down", "Up" ],
255 OpenMenuAccept => "accept" [ "Enter" ],
256 OpenMenuIssue => "issue" [ "i" ],
257 OpenMenuPr => "pr" [ "p" ],
258 }
259 CommandPalette {
260 CommandPaletteClose => "close" [ "Esc" ],
261 CommandPaletteAccept => "accept" [ "Enter" ],
262 CommandPalettePrev => "prev" [ "Up" ],
263 CommandPaletteNext => "next" [ "Down", "Tab" ],
264 }
265 LinkChooseTarget {
266 LinkChooseNext => "next" [ "j", "Down" ],
267 LinkChoosePrev => "prev" [ "k", "Up" ],
268 LinkChooseIssue => "issue" [ "i" ],
269 LinkChoosePr => "pr" [ "p" ],
270 LinkChooseAccept => "accept" [ "Enter" ],
271 LinkChooseCancel => "cancel" [ "Esc" ],
272 }
273 LinkInputNumber {
274 LinkInputSubmit => "submit" [ "Enter" ],
275 LinkInputCancel => "cancel" [ "Esc" ],
276 }
277 ExecPicker {
278 ExecPickerNext => "next" [ "j", "Down" ],
279 ExecPickerPrev => "prev" [ "k", "Up" ],
280 ExecPickerAccept => "accept" [ "Enter" ],
281 ExecPickerCancel => "cancel" [ "Esc" ],
282 }
283 Clean {
284 CleanNext => "next" [ "j", "Down" ],
285 CleanPrev => "prev" [ "k", "Up" ],
286 CleanConfirm => "confirm" [ "y", "Enter" ],
287 CleanCancel => "cancel" [ "n", "Esc" ],
288 }
289}
290
291impl ModalAction {
292 pub fn from_context_verb(ctx: KeyContext, verb: &str) -> Option<Self> {
295 Self::all().find(|a| a.context() == ctx && a.verb() == verb)
296 }
297
298 fn default_keys(self) -> Vec<KeyStroke> {
302 self
303 .default_chord_strs()
304 .iter()
305 .map(|s| {
306 parse_single(s)
307 .unwrap_or_else(|e| panic!("default modal binding {:?} for {:?} failed to parse: {}", s, self, e))
308 })
309 .collect()
310 }
311}
312
313pub fn parse_single(s: &str) -> Result<KeyStroke> {
317 let strokes = KeyStroke::parse_chord(s)?;
318 let stroke = match strokes.into_iter().collect::<Vec<_>>().as_slice() {
319 [one] => one.clone(),
320 _ => {
321 return Err(GwmError::Config(format!(
322 "modal bindings must be a single keystroke, got chord {:?} (modals have no chord timeout)",
323 s
324 )))
325 }
326 };
327 if stroke.modifiers.contains(KeyModifiers::CONTROL)
331 && matches!(stroke.code, KeyCode::Char(c) if c.eq_ignore_ascii_case(&'c'))
332 {
333 return Err(GwmError::Config(format!(
334 "modal bindings cannot use {:?}: Ctrl+C is the reserved emergency quit (handled before any modal lookup)",
335 s
336 )));
337 }
338 Ok(stroke)
339}
340
341#[derive(Debug, Clone)]
348pub struct ModalBinding {
349 pub action: ModalAction,
350 pub keys: Vec<KeyStroke>,
351 pub source: Source,
352}
353
354#[derive(Debug, Clone)]
358pub struct ModalKeymap {
359 entries: Vec<ModalBinding>,
360}
361
362impl ModalKeymap {
363 pub fn defaults() -> Self {
366 let entries = ModalAction::all()
367 .map(|action| ModalBinding {
368 action,
369 keys: action.default_keys(),
370 source: Source::Default,
371 })
372 .collect();
373 Self { entries }
374 }
375
376 pub fn apply_override(&mut self, action: ModalAction, keys: Vec<KeyStroke>) -> Result<()> {
385 let ctx = action.context();
386 let claimed: Vec<&KeyStroke> = keys.iter().collect();
387
388 let mut map: HashMap<KeyStroke, ModalAction> = HashMap::new();
391 for b in &self.entries {
392 if b.action.context() != ctx || b.action == action {
393 continue;
394 }
395 for k in &b.keys {
396 if b.source == Source::Default && claimed.contains(&k) {
397 continue;
398 }
399 map.insert(k.clone(), b.action);
400 }
401 }
402 for k in &keys {
403 if let Some(prev) = map.get(k) {
404 return Err(GwmError::Config(format!(
405 "context {}: key {} bound to both {:?} and {:?} — conflict",
406 ctx.config_path(),
407 k,
408 prev.verb(),
409 action.verb()
410 )));
411 }
412 }
413
414 let claimed_owned: Vec<KeyStroke> = keys.clone();
417 for entry in self.entries.iter_mut() {
418 if entry.action.context() == ctx && entry.action != action && entry.source == Source::Default {
419 entry.keys.retain(|k| !claimed_owned.contains(k));
420 }
421 }
422 if let Some(entry) = self.entries.iter_mut().find(|b| b.action == action) {
423 entry.keys = keys;
424 entry.source = Source::UserConfig;
425 } else {
426 self.entries.push(ModalBinding {
427 action,
428 keys,
429 source: Source::UserConfig,
430 });
431 }
432 Ok(())
433 }
434
435 pub fn resolve(&self, ctx: KeyContext, stroke: &KeyStroke) -> Option<ModalAction> {
439 self
440 .entries
441 .iter()
442 .filter(|b| b.action.context() == ctx)
443 .find(|b| b.keys.iter().any(|k| k == stroke))
444 .map(|b| b.action)
445 }
446
447 pub fn bindings_for(&self, ctx: KeyContext) -> Vec<&ModalBinding> {
449 self.entries.iter().filter(|b| b.action.context() == ctx).collect()
450 }
451
452 pub fn list(&self) -> &[ModalBinding] {
455 &self.entries
456 }
457
458 pub fn primary_key(&self, action: ModalAction) -> Option<String> {
463 self
464 .entries
465 .iter()
466 .find(|b| b.action == action)
467 .and_then(|b| b.keys.first())
468 .map(|k| k.to_string())
469 }
470
471 pub fn keys_display(&self, action: ModalAction) -> String {
475 self
476 .entries
477 .iter()
478 .find(|b| b.action == action)
479 .map(|b| b.keys.iter().map(|k| k.to_string()).collect::<Vec<_>>().join(", "))
480 .unwrap_or_default()
481 }
482}
483
484impl Default for ModalKeymap {
485 fn default() -> Self {
486 Self::defaults()
487 }
488}