notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

keybindings.rs (8142B)


      1 use crate::config::AiMode;
      2 use egui::Key;
      3 
      4 /// Keybinding actions that can be triggered globally
      5 #[derive(Debug, Clone, PartialEq)]
      6 pub enum KeyAction {
      7     /// Accept/Allow a pending permission request
      8     AcceptPermission,
      9     /// Deny a pending permission request
     10     DenyPermission,
     11     /// Tentatively accept, waiting for message (Shift+1)
     12     TentativeAccept,
     13     /// Tentatively deny, waiting for message (Shift+2)
     14     TentativeDeny,
     15     /// Cancel tentative state (Escape when tentative)
     16     CancelTentative,
     17     /// Switch to agent by number (0-indexed)
     18     SwitchToAgent(usize),
     19     /// Cycle to next agent
     20     NextAgent,
     21     /// Cycle to previous agent
     22     PreviousAgent,
     23     /// Spawn a new agent (Ctrl+T)
     24     NewAgent,
     25     /// Interrupt/stop the current AI operation
     26     Interrupt,
     27     /// Toggle between scene view and classic view
     28     ToggleView,
     29     /// Toggle plan mode for the active session (Ctrl+M)
     30     TogglePlanMode,
     31     /// Delete the active session
     32     DeleteActiveSession,
     33     /// Navigate to next item in focus queue (Ctrl+N)
     34     FocusQueueNext,
     35     /// Navigate to previous item in focus queue (Ctrl+P)
     36     FocusQueuePrev,
     37     /// Toggle Done status for current focus queue item (Ctrl+D)
     38     FocusQueueToggleDone,
     39     /// Toggle auto-steal focus mode (Ctrl+\)
     40     ToggleAutoSteal,
     41     /// Open external editor for composing input (Ctrl+G)
     42     OpenExternalEditor,
     43     /// Clone the active agent with the same working directory (Ctrl+Shift+T)
     44     CloneAgent,
     45 }
     46 
     47 /// Check for keybinding actions.
     48 /// Most keybindings use Ctrl modifier to avoid conflicts with text input.
     49 /// Exception: 1/2 for permission responses work without Ctrl but only when no text input has focus.
     50 /// In Chat mode, agentic-specific keybindings (scene view, plan mode, focus queue) are disabled.
     51 pub fn check_keybindings(
     52     ctx: &egui::Context,
     53     has_pending_permission: bool,
     54     has_pending_question: bool,
     55     in_tentative_state: bool,
     56     ai_mode: AiMode,
     57 ) -> Option<KeyAction> {
     58     let is_agentic = ai_mode == AiMode::Agentic;
     59 
     60     // Escape in tentative state cancels the tentative mode (agentic only)
     61     if is_agentic && in_tentative_state && ctx.input(|i| i.key_pressed(Key::Escape)) {
     62         return Some(KeyAction::CancelTentative);
     63     }
     64 
     65     // Escape otherwise works to interrupt AI (even when text input has focus)
     66     if ctx.input(|i| i.key_pressed(Key::Escape)) {
     67         return Some(KeyAction::Interrupt);
     68     }
     69 
     70     let ctrl = egui::Modifiers::CTRL;
     71     let ctrl_shift = egui::Modifiers::CTRL | egui::Modifiers::SHIFT;
     72 
     73     // Ctrl+Tab / Ctrl+Shift+Tab for cycling through agents/chats
     74     // Works even with text input focus since Ctrl modifier makes it unambiguous
     75     // IMPORTANT: Check Ctrl+Shift+Tab first because consume_key uses matches_logically
     76     // which ignores extra Shift, so Ctrl+Tab would consume Ctrl+Shift+Tab otherwise
     77     if let Some(action) = ctx.input_mut(|i| {
     78         if i.consume_key(ctrl_shift, Key::Tab) {
     79             Some(KeyAction::PreviousAgent)
     80         } else if i.consume_key(ctrl, Key::Tab) {
     81             Some(KeyAction::NextAgent)
     82         } else {
     83             None
     84         }
     85     }) {
     86         return Some(action);
     87     }
     88 
     89     // Focus queue navigation - agentic only
     90     if is_agentic {
     91         // Ctrl+N for higher priority (toward NeedsInput)
     92         if ctx.input(|i| i.modifiers.matches_exact(ctrl) && i.key_pressed(Key::N)) {
     93             return Some(KeyAction::FocusQueueNext);
     94         }
     95 
     96         // Ctrl+P for lower priority (toward Done)
     97         if ctx.input(|i| i.modifiers.matches_exact(ctrl) && i.key_pressed(Key::P)) {
     98             return Some(KeyAction::FocusQueuePrev);
     99         }
    100     }
    101 
    102     // Ctrl+Shift+T to clone the active agent (check before Ctrl+T) - agentic only
    103     if is_agentic && ctx.input(|i| i.modifiers.matches_exact(ctrl_shift) && i.key_pressed(Key::T)) {
    104         return Some(KeyAction::CloneAgent);
    105     }
    106 
    107     // Ctrl+T to spawn a new agent/chat
    108     if ctx.input(|i| i.modifiers.matches_exact(ctrl) && i.key_pressed(Key::T)) {
    109         return Some(KeyAction::NewAgent);
    110     }
    111 
    112     // Ctrl+L to toggle between scene view and list view - agentic only
    113     if is_agentic && ctx.input(|i| i.modifiers.matches_exact(ctrl) && i.key_pressed(Key::L)) {
    114         return Some(KeyAction::ToggleView);
    115     }
    116 
    117     // Ctrl+G to open external editor for composing input
    118     if ctx.input(|i| i.modifiers.matches_exact(ctrl) && i.key_pressed(Key::G)) {
    119         return Some(KeyAction::OpenExternalEditor);
    120     }
    121 
    122     // Ctrl+M to toggle plan mode - agentic only
    123     if is_agentic && ctx.input(|i| i.modifiers.matches_exact(ctrl) && i.key_pressed(Key::M)) {
    124         return Some(KeyAction::TogglePlanMode);
    125     }
    126 
    127     // Ctrl+D to toggle Done status for current focus queue item - agentic only
    128     if is_agentic && ctx.input(|i| i.modifiers.matches_exact(ctrl) && i.key_pressed(Key::D)) {
    129         return Some(KeyAction::FocusQueueToggleDone);
    130     }
    131 
    132     // Ctrl+\ to toggle auto-steal focus mode (Ctrl+Space conflicts with macOS input source switching) - agentic only
    133     if is_agentic && ctx.input(|i| i.modifiers.matches_exact(ctrl) && i.key_pressed(Key::Backslash))
    134     {
    135         return Some(KeyAction::ToggleAutoSteal);
    136     }
    137 
    138     // Delete key to delete active session (only when no text input has focus)
    139     if !ctx.wants_keyboard_input() && ctx.input(|i| i.key_pressed(Key::Delete)) {
    140         return Some(KeyAction::DeleteActiveSession);
    141     }
    142 
    143     // Ctrl+1-9 for switching agents/chats (works even with text input focus)
    144     // Check this BEFORE permission bindings so Ctrl+number always switches agents
    145     if let Some(action) = ctx.input(|i| {
    146         if !i.modifiers.matches_exact(ctrl) {
    147             return None;
    148         }
    149 
    150         for (idx, key) in [
    151             Key::Num1,
    152             Key::Num2,
    153             Key::Num3,
    154             Key::Num4,
    155             Key::Num5,
    156             Key::Num6,
    157             Key::Num7,
    158             Key::Num8,
    159             Key::Num9,
    160         ]
    161         .iter()
    162         .enumerate()
    163         {
    164             if i.key_pressed(*key) {
    165                 return Some(KeyAction::SwitchToAgent(idx));
    166             }
    167         }
    168 
    169         None
    170     }) {
    171         return Some(action);
    172     }
    173 
    174     // Permission keybindings - agentic only
    175     // When there's a pending permission (but NOT an AskUserQuestion):
    176     // - 1 = accept, 2 = deny (no modifiers)
    177     // - Shift+1 = tentative accept, Shift+2 = tentative deny (for adding message)
    178     // This is checked AFTER Ctrl+number so Ctrl bindings take precedence
    179     // IMPORTANT: Only handle these when no text input has focus, to avoid
    180     // capturing keypresses when user is typing a message in tentative state
    181     // AskUserQuestion uses number keys for option selection, so we skip these bindings
    182     if is_agentic && has_pending_permission && !has_pending_question && !ctx.wants_keyboard_input()
    183     {
    184         // Shift+1 = tentative accept, Shift+2 = tentative deny
    185         // Note: egui may report shifted keys as their symbol (e.g., Shift+1 as Exclamationmark)
    186         // We check for both the symbol key and Shift+Num key to handle different behaviors
    187         if let Some(action) = ctx.input_mut(|i| {
    188             // Shift+1: check for '!' (Exclamationmark) which egui reports on some systems
    189             if i.key_pressed(Key::Exclamationmark) {
    190                 return Some(KeyAction::TentativeAccept);
    191             }
    192             // Shift+2: check with shift modifier (egui may report Num2 with shift held)
    193             if i.modifiers.shift && i.key_pressed(Key::Num2) {
    194                 return Some(KeyAction::TentativeDeny);
    195             }
    196             None
    197         }) {
    198             return Some(action);
    199         }
    200 
    201         // Bare keypresses (no modifiers) for immediate accept/deny
    202         if let Some(action) = ctx.input(|i| {
    203             if !i.modifiers.any() {
    204                 if i.key_pressed(Key::Num1) {
    205                     return Some(KeyAction::AcceptPermission);
    206                 } else if i.key_pressed(Key::Num2) {
    207                     return Some(KeyAction::DenyPermission);
    208                 }
    209             }
    210             None
    211         }) {
    212             return Some(action);
    213         }
    214     }
    215 
    216     None
    217 }