test fixes for arch linux builds
- SHA
ddc3697a8314e3fd20a14d7c0a21085cb51dd63e- Parents
-
798b461 - Tree
5990a95
ddc3697
ddc3697a8314e3fd20a14d7c0a21085cb51dd63e798b461
5990a95| Status | File | + | - |
|---|---|---|---|
| M |
Cargo.toml
|
5 | 1 |
| M |
fackr.spec
|
29 | 1 |
| A |
src/bin/keytest.rs
|
62 | 0 |
| M |
src/editor/state.rs
|
274 | 1 |
| M |
src/render/screen.rs
|
209 | 0 |
Cargo.tomlmodified@@ -7,7 +7,7 @@ authors = ["Matthew Forrester Wolffe"] | ||
| 7 | 7 | |
| 8 | 8 | [dependencies] |
| 9 | 9 | # Terminal |
| 10 | -crossterm = "0.28" | |
| 10 | +crossterm = { version = "0.28", features = ["libc"] } | |
| 11 | 11 | |
| 12 | 12 | # Text handling |
| 13 | 13 | ropey = "1.6" |
@@ -39,6 +39,10 @@ path = "src/main.rs" | ||
| 39 | 39 | name = "fac" |
| 40 | 40 | path = "src/main.rs" |
| 41 | 41 | |
| 42 | +[[bin]] | |
| 43 | +name = "keytest" | |
| 44 | +path = "src/bin/keytest.rs" | |
| 45 | + | |
| 42 | 46 | [profile.release] |
| 43 | 47 | opt-level = 3 |
| 44 | 48 | lto = true |
fackr.specmodified@@ -1,5 +1,5 @@ | ||
| 1 | 1 | Name: fackr |
| 2 | -Version: 0.4.0 | |
| 2 | +Version: 0.9.6 | |
| 3 | 3 | Release: 1%{?dist} |
| 4 | 4 | Summary: Terminal text editor written in Rust |
| 5 | 5 | |
@@ -49,6 +49,34 @@ install -Dm644 README.md %{buildroot}%{_docdir}/%{name}/README.md 2>/dev/null || | ||
| 49 | 49 | %{_bindir}/fac |
| 50 | 50 | |
| 51 | 51 | %changelog |
| 52 | +* Wed Dec 11 2024 mfw <espadon@outlook.com> - 0.9.6-1 | |
| 53 | +- Fix command palette char input | |
| 54 | +- Create new file from CLI | |
| 55 | + | |
| 56 | +* Wed Dec 11 2024 mfw <espadon@outlook.com> - 0.9.5-1 | |
| 57 | +- Command palette (Ctrl+P) with fuzzy search | |
| 58 | + | |
| 59 | +* Wed Dec 11 2024 mfw <espadon@outlook.com> - 0.9.4-1 | |
| 60 | +- Ctrl+D scrolls viewport to show newly added cursor | |
| 61 | + | |
| 62 | +* Tue Dec 10 2024 mfw <espadon@outlook.com> - 0.9.0-1 | |
| 63 | +- Add Ctrl+/ toggle line comment | |
| 64 | +- Performance optimizations | |
| 65 | + | |
| 66 | +* Mon Dec 09 2024 mfw <espadon@outlook.com> - 0.8.0-1 | |
| 67 | +- Add Ctrl+G/F5 goto line with line:col syntax | |
| 68 | +- Add Ctrl+O fortress file browser | |
| 69 | +- Fix multi-cursor same-line edits | |
| 70 | + | |
| 71 | +* Mon Dec 09 2024 mfw <espadon@outlook.com> - 0.7.0-1 | |
| 72 | +- Add Ctrl+F/Ctrl+R find and replace with regex support | |
| 73 | +- Fuzzy filter improvements and references panel visual fix | |
| 74 | + | |
| 75 | +* Sun Dec 08 2024 mfw <espadon@outlook.com> - 0.6.0-1 | |
| 76 | +- Add Shift+F12 references panel with filtering | |
| 77 | +- Syntax highlighting for 30+ languages | |
| 78 | +- LSP support with auto-completion and diagnostics | |
| 79 | + | |
| 52 | 80 | * Sat Dec 07 2024 mfw <espadon@outlook.com> - 0.4.0-1 |
| 53 | 81 | - Initial RPM release of fackr |
| 54 | 82 | - Terminal text editor written in Rust |
src/bin/keytest.rsadded@@ -0,0 +1,62 @@ | ||
| 1 | +// Simple key event debugger | |
| 2 | +// Run with: cargo run --bin keytest | |
| 3 | + | |
| 4 | +use crossterm::{ | |
| 5 | + event::{self, Event, KeyCode, KeyEvent, KeyModifiers, PushKeyboardEnhancementFlags, PopKeyboardEnhancementFlags, KeyboardEnhancementFlags}, | |
| 6 | + terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}, | |
| 7 | + execute, | |
| 8 | +}; | |
| 9 | +use std::io::{self, Write}; | |
| 10 | + | |
| 11 | +fn main() -> io::Result<()> { | |
| 12 | + terminal::enable_raw_mode()?; | |
| 13 | + execute!(io::stdout(), EnterAlternateScreen)?; | |
| 14 | + | |
| 15 | + // Try to enable keyboard enhancement | |
| 16 | + let enhanced = execute!( | |
| 17 | + io::stdout(), | |
| 18 | + PushKeyboardEnhancementFlags( | |
| 19 | + KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES | |
| 20 | + | KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES | |
| 21 | + ) | |
| 22 | + ).is_ok(); | |
| 23 | + | |
| 24 | + println!("Keyboard enhancement: {}\r", if enhanced { "enabled" } else { "disabled" }); | |
| 25 | + println!("Press keys to see events (Ctrl+C to quit)\r\n"); | |
| 26 | + | |
| 27 | + loop { | |
| 28 | + if event::poll(std::time::Duration::from_millis(100))? { | |
| 29 | + match event::read()? { | |
| 30 | + Event::Key(KeyEvent { code, modifiers, kind, state }) => { | |
| 31 | + let ctrl = modifiers.contains(KeyModifiers::CONTROL); | |
| 32 | + let alt = modifiers.contains(KeyModifiers::ALT); | |
| 33 | + let shift = modifiers.contains(KeyModifiers::SHIFT); | |
| 34 | + | |
| 35 | + println!( | |
| 36 | + "Key: {:?} | Modifiers: ctrl={} alt={} shift={} | Kind: {:?} | State: {:?}\r", | |
| 37 | + code, ctrl, alt, shift, kind, state | |
| 38 | + ); | |
| 39 | + | |
| 40 | + // Quit on Ctrl+C | |
| 41 | + if let KeyCode::Char('c') = code { | |
| 42 | + if ctrl { | |
| 43 | + break; | |
| 44 | + } | |
| 45 | + } | |
| 46 | + } | |
| 47 | + Event::Resize(w, h) => { | |
| 48 | + println!("Resize: {}x{}\r", w, h); | |
| 49 | + } | |
| 50 | + _ => {} | |
| 51 | + } | |
| 52 | + } | |
| 53 | + } | |
| 54 | + | |
| 55 | + if enhanced { | |
| 56 | + let _ = execute!(io::stdout(), PopKeyboardEnhancementFlags); | |
| 57 | + } | |
| 58 | + execute!(io::stdout(), LeaveAlternateScreen)?; | |
| 59 | + terminal::disable_raw_mode()?; | |
| 60 | + | |
| 61 | + Ok(()) | |
| 62 | +} | |
src/editor/state.rsmodified@@ -128,6 +128,134 @@ const ALL_COMMANDS: &[PaletteCommand] = &[ | ||
| 128 | 128 | |
| 129 | 129 | // Help |
| 130 | 130 | PaletteCommand::new("Command Palette", "Ctrl+P", "Help", "command-palette"), |
| 131 | + PaletteCommand::new("Help / Keybindings", "Shift+F1", "Help", "help"), | |
| 132 | +]; | |
| 133 | + | |
| 134 | +/// A keybinding entry for the help menu | |
| 135 | +#[derive(Debug, Clone, PartialEq)] | |
| 136 | +struct HelpKeybind { | |
| 137 | + /// Keyboard shortcut (e.g., "Ctrl+S") | |
| 138 | + shortcut: &'static str, | |
| 139 | + /// Description of what the keybind does | |
| 140 | + description: &'static str, | |
| 141 | + /// Category for grouping | |
| 142 | + category: &'static str, | |
| 143 | +} | |
| 144 | + | |
| 145 | +impl HelpKeybind { | |
| 146 | + const fn new(shortcut: &'static str, description: &'static str, category: &'static str) -> Self { | |
| 147 | + Self { shortcut, description, category } | |
| 148 | + } | |
| 149 | +} | |
| 150 | + | |
| 151 | +/// All keybindings for the help menu - comprehensive list | |
| 152 | +const ALL_KEYBINDS: &[HelpKeybind] = &[ | |
| 153 | + // File Operations | |
| 154 | + HelpKeybind::new("Ctrl+S", "Save file", "File"), | |
| 155 | + HelpKeybind::new("Ctrl+O", "Open file browser (Fortress)", "File"), | |
| 156 | + HelpKeybind::new("Ctrl+Q", "Quit editor", "File"), | |
| 157 | + HelpKeybind::new("Ctrl+B / F3", "Toggle file explorer", "File"), | |
| 158 | + | |
| 159 | + // Tabs | |
| 160 | + HelpKeybind::new("Alt+T", "New tab", "Tabs"), | |
| 161 | + HelpKeybind::new("Alt+Q", "Close tab/pane", "Tabs"), | |
| 162 | + HelpKeybind::new("Alt+.", "Next tab", "Tabs"), | |
| 163 | + HelpKeybind::new("Alt+,", "Previous tab", "Tabs"), | |
| 164 | + HelpKeybind::new("Alt+1-9", "Switch to tab 1-9", "Tabs"), | |
| 165 | + | |
| 166 | + // Panes | |
| 167 | + HelpKeybind::new("Alt+V", "Split vertical", "Panes"), | |
| 168 | + HelpKeybind::new("Alt+S", "Split horizontal", "Panes"), | |
| 169 | + HelpKeybind::new("Alt+H/J/K/L", "Navigate panes (vim-style)", "Panes"), | |
| 170 | + HelpKeybind::new("Alt+N", "Next pane", "Panes"), | |
| 171 | + HelpKeybind::new("Alt+P", "Previous pane", "Panes"), | |
| 172 | + | |
| 173 | + // Editing | |
| 174 | + HelpKeybind::new("Ctrl+Z", "Undo", "Edit"), | |
| 175 | + HelpKeybind::new("Ctrl+Shift+Z", "Redo", "Edit"), | |
| 176 | + HelpKeybind::new("Ctrl+C", "Copy", "Edit"), | |
| 177 | + HelpKeybind::new("Ctrl+X", "Cut", "Edit"), | |
| 178 | + HelpKeybind::new("Ctrl+V", "Paste", "Edit"), | |
| 179 | + HelpKeybind::new("Ctrl+J", "Join lines", "Edit"), | |
| 180 | + HelpKeybind::new("Ctrl+/", "Toggle line comment", "Edit"), | |
| 181 | + HelpKeybind::new("Ctrl+T", "Transpose characters", "Edit"), | |
| 182 | + HelpKeybind::new("Tab", "Indent", "Edit"), | |
| 183 | + HelpKeybind::new("Shift+Tab", "Outdent", "Edit"), | |
| 184 | + HelpKeybind::new("Backspace", "Delete backward", "Edit"), | |
| 185 | + HelpKeybind::new("Delete", "Delete forward", "Edit"), | |
| 186 | + HelpKeybind::new("Ctrl+W", "Delete word backward", "Edit"), | |
| 187 | + HelpKeybind::new("Alt+D", "Delete word forward", "Edit"), | |
| 188 | + HelpKeybind::new("Alt+Backspace", "Delete word backward", "Edit"), | |
| 189 | + | |
| 190 | + // Line Operations | |
| 191 | + HelpKeybind::new("Alt+Up", "Move line up", "Lines"), | |
| 192 | + HelpKeybind::new("Alt+Down", "Move line down", "Lines"), | |
| 193 | + HelpKeybind::new("Alt+Shift+Up", "Duplicate line up", "Lines"), | |
| 194 | + HelpKeybind::new("Alt+Shift+Down", "Duplicate line down", "Lines"), | |
| 195 | + | |
| 196 | + // Movement | |
| 197 | + HelpKeybind::new("Arrow keys", "Move cursor", "Movement"), | |
| 198 | + HelpKeybind::new("Home / Ctrl+A", "Go to line start (smart)", "Movement"), | |
| 199 | + HelpKeybind::new("End / Ctrl+E", "Go to line end", "Movement"), | |
| 200 | + HelpKeybind::new("Alt+Left / Alt+B", "Move word left", "Movement"), | |
| 201 | + HelpKeybind::new("Alt+Right / Alt+F", "Move word right", "Movement"), | |
| 202 | + HelpKeybind::new("PageUp", "Page up", "Movement"), | |
| 203 | + HelpKeybind::new("PageDown", "Page down", "Movement"), | |
| 204 | + HelpKeybind::new("Ctrl+G / F5", "Go to line", "Movement"), | |
| 205 | + | |
| 206 | + // Selection | |
| 207 | + HelpKeybind::new("Shift+Arrow", "Extend selection", "Selection"), | |
| 208 | + HelpKeybind::new("Ctrl+L", "Select line", "Selection"), | |
| 209 | + HelpKeybind::new("Ctrl+D", "Select word / next occurrence", "Selection"), | |
| 210 | + HelpKeybind::new("Escape", "Clear selection / collapse cursors", "Selection"), | |
| 211 | + HelpKeybind::new("Ctrl+Alt+Up", "Add cursor above", "Selection"), | |
| 212 | + HelpKeybind::new("Ctrl+Alt+Down", "Add cursor below", "Selection"), | |
| 213 | + | |
| 214 | + // Search | |
| 215 | + HelpKeybind::new("Ctrl+F", "Find", "Search"), | |
| 216 | + HelpKeybind::new("Ctrl+R", "Find and replace", "Search"), | |
| 217 | + HelpKeybind::new("F3", "Find next", "Search"), | |
| 218 | + HelpKeybind::new("Shift+F3", "Find previous", "Search"), | |
| 219 | + HelpKeybind::new("F4", "Search in files", "Search"), | |
| 220 | + HelpKeybind::new("Alt+I", "Toggle case sensitivity (in find)", "Search"), | |
| 221 | + HelpKeybind::new("Alt+X", "Toggle regex mode (in find)", "Search"), | |
| 222 | + HelpKeybind::new("Alt+Enter", "Replace all (in find)", "Search"), | |
| 223 | + | |
| 224 | + // Brackets & Quotes | |
| 225 | + HelpKeybind::new("Alt+[ / Alt+]", "Jump to matching bracket", "Brackets"), | |
| 226 | + HelpKeybind::new("Alt+'", "Cycle quote type (\"/'/`)", "Brackets"), | |
| 227 | + HelpKeybind::new("Alt+\"", "Remove surrounding quotes", "Brackets"), | |
| 228 | + HelpKeybind::new("Alt+(", "Cycle bracket type (/{/[)", "Brackets"), | |
| 229 | + HelpKeybind::new("Alt+)", "Remove surrounding brackets", "Brackets"), | |
| 230 | + | |
| 231 | + // LSP / Code Intelligence | |
| 232 | + HelpKeybind::new("F1", "Show hover info", "LSP"), | |
| 233 | + HelpKeybind::new("F2", "Rename symbol", "LSP"), | |
| 234 | + HelpKeybind::new("F12", "Go to definition", "LSP"), | |
| 235 | + HelpKeybind::new("Shift+F12", "Find references", "LSP"), | |
| 236 | + HelpKeybind::new("Ctrl+N", "Trigger completion", "LSP"), | |
| 237 | + HelpKeybind::new("Alt+M", "LSP server manager", "LSP"), | |
| 238 | + | |
| 239 | + // Help & Commands | |
| 240 | + HelpKeybind::new("Ctrl+P", "Command palette", "Help"), | |
| 241 | + HelpKeybind::new("Shift+F1", "Help / keybindings", "Help"), | |
| 242 | + | |
| 243 | + // File Explorer (Fortress/Fuss mode) | |
| 244 | + HelpKeybind::new("Up/Down", "Navigate files", "Explorer"), | |
| 245 | + HelpKeybind::new("Enter", "Open file/directory", "Explorer"), | |
| 246 | + HelpKeybind::new("Right", "Expand directory", "Explorer"), | |
| 247 | + HelpKeybind::new("Left", "Collapse / go to parent", "Explorer"), | |
| 248 | + HelpKeybind::new("Space", "Toggle selection", "Explorer"), | |
| 249 | + HelpKeybind::new("a", "Add file", "Explorer"), | |
| 250 | + HelpKeybind::new("d", "Delete selected", "Explorer"), | |
| 251 | + HelpKeybind::new("m", "Move/rename selected", "Explorer"), | |
| 252 | + HelpKeybind::new("p", "Paste", "Explorer"), | |
| 253 | + HelpKeybind::new("u", "Undo last action", "Explorer"), | |
| 254 | + HelpKeybind::new("f", "Create folder", "Explorer"), | |
| 255 | + HelpKeybind::new("t", "Open in new tab", "Explorer"), | |
| 256 | + HelpKeybind::new("l", "Open in vertical split", "Explorer"), | |
| 257 | + HelpKeybind::new("Alt+G", "Git status", "Explorer"), | |
| 258 | + HelpKeybind::new("Alt+.", "Toggle hidden files", "Explorer"), | |
| 131 | 259 | ]; |
| 132 | 260 | |
| 133 | 261 | /// Prompt state for quit confirmation |
@@ -206,6 +334,17 @@ enum PromptState { | ||
| 206 | 334 | /// Scroll offset for long lists |
| 207 | 335 | scroll_offset: usize, |
| 208 | 336 | }, |
| 337 | + /// Help menu (Shift+F1) | |
| 338 | + HelpMenu { | |
| 339 | + /// Search/filter query | |
| 340 | + query: String, | |
| 341 | + /// Filtered keybinds matching query | |
| 342 | + filtered: Vec<HelpKeybind>, | |
| 343 | + /// Currently selected index | |
| 344 | + selected_index: usize, | |
| 345 | + /// Scroll offset for long lists | |
| 346 | + scroll_offset: usize, | |
| 347 | + }, | |
| 209 | 348 | } |
| 210 | 349 | |
| 211 | 350 | /// A single result from multi-file search |
@@ -1588,6 +1727,27 @@ impl Editor { | ||
| 1588 | 1727 | return Ok(()); // Modal handles cursor |
| 1589 | 1728 | } |
| 1590 | 1729 | |
| 1730 | + // Render help menu if active | |
| 1731 | + if let PromptState::HelpMenu { | |
| 1732 | + ref query, | |
| 1733 | + ref filtered, | |
| 1734 | + selected_index, | |
| 1735 | + scroll_offset, | |
| 1736 | + } = self.prompt { | |
| 1737 | + // Convert keybinds to tuple format for render function | |
| 1738 | + let keybinds_tuples: Vec<(String, String, String)> = filtered | |
| 1739 | + .iter() | |
| 1740 | + .map(|kb| (kb.shortcut.to_string(), kb.description.to_string(), kb.category.to_string())) | |
| 1741 | + .collect(); | |
| 1742 | + self.screen.render_help_menu( | |
| 1743 | + query, | |
| 1744 | + &keybinds_tuples, | |
| 1745 | + selected_index, | |
| 1746 | + scroll_offset, | |
| 1747 | + )?; | |
| 1748 | + return Ok(()); // Modal handles cursor | |
| 1749 | + } | |
| 1750 | + | |
| 1591 | 1751 | // Render find/replace bar if active (replaces status bar) |
| 1592 | 1752 | if let PromptState::FindReplace { |
| 1593 | 1753 | ref find_query, |
@@ -1964,7 +2124,7 @@ impl Editor { | ||
| 1964 | 2124 | // Find references: Shift+F12 |
| 1965 | 2125 | (Key::F(12), Modifiers { shift: true, .. }) => self.lsp_find_references(), |
| 1966 | 2126 | // Hover info: F1 |
| 1967 | - (Key::F(1), _) => self.lsp_hover(), | |
| 2127 | + (Key::F(1), Modifiers { shift: false, .. }) => self.lsp_hover(), | |
| 1968 | 2128 | // Code completion: Ctrl+N (vim-style) |
| 1969 | 2129 | (Key::Char('n'), Modifiers { ctrl: true, .. }) => self.lsp_complete(), |
| 1970 | 2130 | // Rename: F2 |
@@ -1972,6 +2132,10 @@ impl Editor { | ||
| 1972 | 2132 | // Server manager: Alt+M |
| 1973 | 2133 | (Key::Char('m'), Modifiers { alt: true, .. }) => self.toggle_server_manager(), |
| 1974 | 2134 | |
| 2135 | + // === Help === | |
| 2136 | + // Help / keybindings: Shift+F1 | |
| 2137 | + (Key::F(1), Modifiers { shift: true, .. }) => self.open_help_menu(), | |
| 2138 | + | |
| 1975 | 2139 | _ => {} |
| 1976 | 2140 | } |
| 1977 | 2141 | |
@@ -4616,6 +4780,72 @@ impl Editor { | ||
| 4616 | 4780 | _ => {} |
| 4617 | 4781 | } |
| 4618 | 4782 | } |
| 4783 | + PromptState::HelpMenu { | |
| 4784 | + ref mut query, | |
| 4785 | + ref mut filtered, | |
| 4786 | + ref mut selected_index, | |
| 4787 | + ref mut scroll_offset, | |
| 4788 | + } => { | |
| 4789 | + match key { | |
| 4790 | + Key::Escape | Key::Enter => { | |
| 4791 | + self.prompt = PromptState::None; | |
| 4792 | + } | |
| 4793 | + Key::Up => { | |
| 4794 | + if *selected_index > 0 { | |
| 4795 | + *selected_index -= 1; | |
| 4796 | + if *selected_index < *scroll_offset { | |
| 4797 | + *scroll_offset = *selected_index; | |
| 4798 | + } | |
| 4799 | + } | |
| 4800 | + } | |
| 4801 | + Key::Down => { | |
| 4802 | + if *selected_index + 1 < filtered.len() { | |
| 4803 | + *selected_index += 1; | |
| 4804 | + let visible_rows = 18; | |
| 4805 | + if *selected_index >= *scroll_offset + visible_rows { | |
| 4806 | + *scroll_offset = selected_index.saturating_sub(visible_rows - 1); | |
| 4807 | + } | |
| 4808 | + } | |
| 4809 | + } | |
| 4810 | + Key::PageUp => { | |
| 4811 | + *selected_index = selected_index.saturating_sub(10); | |
| 4812 | + if *selected_index < *scroll_offset { | |
| 4813 | + *scroll_offset = *selected_index; | |
| 4814 | + } | |
| 4815 | + } | |
| 4816 | + Key::PageDown => { | |
| 4817 | + *selected_index = (*selected_index + 10).min(filtered.len().saturating_sub(1)); | |
| 4818 | + let visible_rows = 18; | |
| 4819 | + if *selected_index >= *scroll_offset + visible_rows { | |
| 4820 | + *scroll_offset = selected_index.saturating_sub(visible_rows - 1); | |
| 4821 | + } | |
| 4822 | + } | |
| 4823 | + Key::Home => { | |
| 4824 | + *selected_index = 0; | |
| 4825 | + *scroll_offset = 0; | |
| 4826 | + } | |
| 4827 | + Key::End => { | |
| 4828 | + *selected_index = filtered.len().saturating_sub(1); | |
| 4829 | + let visible_rows = 18; | |
| 4830 | + *scroll_offset = selected_index.saturating_sub(visible_rows - 1); | |
| 4831 | + } | |
| 4832 | + Key::Backspace => { | |
| 4833 | + if !query.is_empty() { | |
| 4834 | + query.pop(); | |
| 4835 | + *filtered = filter_keybinds(query); | |
| 4836 | + *selected_index = 0; | |
| 4837 | + *scroll_offset = 0; | |
| 4838 | + } | |
| 4839 | + } | |
| 4840 | + Key::Char(c) => { | |
| 4841 | + query.push(c); | |
| 4842 | + *filtered = filter_keybinds(query); | |
| 4843 | + *selected_index = 0; | |
| 4844 | + *scroll_offset = 0; | |
| 4845 | + } | |
| 4846 | + _ => {} | |
| 4847 | + } | |
| 4848 | + } | |
| 4619 | 4849 | PromptState::None => {} |
| 4620 | 4850 | } |
| 4621 | 4851 | Ok(()) |
@@ -5472,12 +5702,26 @@ impl Editor { | ||
| 5472 | 5702 | |
| 5473 | 5703 | // Help |
| 5474 | 5704 | "command-palette" => {} // Already open |
| 5705 | + "help" => self.open_help_menu(), | |
| 5475 | 5706 | |
| 5476 | 5707 | _ => { |
| 5477 | 5708 | self.message = Some(format!("Unknown command: {}", command_id)); |
| 5478 | 5709 | } |
| 5479 | 5710 | } |
| 5480 | 5711 | } |
| 5712 | + | |
| 5713 | + // === Help Menu === | |
| 5714 | + | |
| 5715 | + /// Open the help menu with keybindings | |
| 5716 | + fn open_help_menu(&mut self) { | |
| 5717 | + let filtered = filter_keybinds(""); | |
| 5718 | + self.prompt = PromptState::HelpMenu { | |
| 5719 | + query: String::new(), | |
| 5720 | + filtered, | |
| 5721 | + selected_index: 0, | |
| 5722 | + scroll_offset: 0, | |
| 5723 | + }; | |
| 5724 | + } | |
| 5481 | 5725 | } |
| 5482 | 5726 | |
| 5483 | 5727 | /// Fuzzy match scoring for command palette |
@@ -5558,6 +5802,35 @@ fn filter_commands(query: &str) -> Vec<PaletteCommand> { | ||
| 5558 | 5802 | filtered |
| 5559 | 5803 | } |
| 5560 | 5804 | |
| 5805 | +/// Filter keybinds by fuzzy match (for help menu) | |
| 5806 | +fn filter_keybinds(query: &str) -> Vec<HelpKeybind> { | |
| 5807 | + if query.is_empty() { | |
| 5808 | + // Return all keybinds in original order (grouped by category) | |
| 5809 | + return ALL_KEYBINDS.to_vec(); | |
| 5810 | + } | |
| 5811 | + | |
| 5812 | + let mut filtered: Vec<(HelpKeybind, i32)> = ALL_KEYBINDS | |
| 5813 | + .iter() | |
| 5814 | + .filter_map(|kb| { | |
| 5815 | + // Match against shortcut, description, or category | |
| 5816 | + let shortcut_score = fuzzy_match_score(kb.shortcut, query); | |
| 5817 | + let desc_score = fuzzy_match_score(kb.description, query); | |
| 5818 | + let category_score = fuzzy_match_score(kb.category, query) / 2; | |
| 5819 | + | |
| 5820 | + let score = shortcut_score.max(desc_score).max(category_score); | |
| 5821 | + if score > 0 { | |
| 5822 | + Some((kb.clone(), score)) | |
| 5823 | + } else { | |
| 5824 | + None | |
| 5825 | + } | |
| 5826 | + }) | |
| 5827 | + .collect(); | |
| 5828 | + | |
| 5829 | + // Sort by score descending | |
| 5830 | + filtered.sort_by(|a, b| b.1.cmp(&a.1)); | |
| 5831 | + filtered.into_iter().map(|(kb, _)| kb).collect() | |
| 5832 | +} | |
| 5833 | + | |
| 5561 | 5834 | impl Drop for Editor { |
| 5562 | 5835 | fn drop(&mut self) { |
| 5563 | 5836 | let _ = self.screen.leave_raw_mode(); |
src/render/screen.rsmodified@@ -2710,6 +2710,215 @@ impl Screen { | ||
| 2710 | 2710 | Ok(()) |
| 2711 | 2711 | } |
| 2712 | 2712 | |
| 2713 | + /// Render the help menu modal (Shift+F1) | |
| 2714 | + pub fn render_help_menu( | |
| 2715 | + &mut self, | |
| 2716 | + query: &str, | |
| 2717 | + keybinds: &[(String, String, String)], // (shortcut, description, category) | |
| 2718 | + selected_index: usize, | |
| 2719 | + scroll_offset: usize, | |
| 2720 | + ) -> Result<()> { | |
| 2721 | + let (width, height) = (self.cols as usize, self.rows as usize); | |
| 2722 | + | |
| 2723 | + // Modal dimensions - larger to show keybindings comfortably | |
| 2724 | + let modal_width = 70.min(width - 4); | |
| 2725 | + let modal_height = 24.min(height - 4); | |
| 2726 | + let start_col = (width.saturating_sub(modal_width)) / 2; | |
| 2727 | + let start_row = 1; // Near top of screen | |
| 2728 | + | |
| 2729 | + // Colors - sleek dark theme matching command palette | |
| 2730 | + let bg = Color::AnsiValue(236); | |
| 2731 | + let border_color = Color::AnsiValue(240); | |
| 2732 | + let title_color = Color::Cyan; | |
| 2733 | + let category_color = Color::AnsiValue(243); | |
| 2734 | + let shortcut_color = Color::Yellow; | |
| 2735 | + let desc_color = Color::White; | |
| 2736 | + let selected_bg = Color::AnsiValue(24); // Blue highlight | |
| 2737 | + let input_bg = Color::AnsiValue(238); | |
| 2738 | + | |
| 2739 | + // Draw top border with title | |
| 2740 | + let title = " Keybindings "; | |
| 2741 | + let title_padding = (modal_width.saturating_sub(title.len() + 2)) / 2; | |
| 2742 | + execute!( | |
| 2743 | + self.stdout, | |
| 2744 | + MoveTo(start_col as u16, start_row as u16), | |
| 2745 | + SetBackgroundColor(bg), | |
| 2746 | + SetForegroundColor(border_color), | |
| 2747 | + Print("╭"), | |
| 2748 | + Print(format!("{:─<width$}", "", width = title_padding)), | |
| 2749 | + SetForegroundColor(title_color), | |
| 2750 | + SetAttribute(crossterm::style::Attribute::Bold), | |
| 2751 | + Print(title), | |
| 2752 | + SetAttribute(crossterm::style::Attribute::Reset), | |
| 2753 | + SetForegroundColor(border_color), | |
| 2754 | + Print(format!("{:─<width$}", "", width = modal_width.saturating_sub(title_padding + title.len() + 2))), | |
| 2755 | + Print("╮"), | |
| 2756 | + ResetColor, | |
| 2757 | + )?; | |
| 2758 | + | |
| 2759 | + // Draw search input row | |
| 2760 | + let display_query = if query.is_empty() { "Type to filter..." } else { query }; | |
| 2761 | + let input_display_width = modal_width.saturating_sub(6); | |
| 2762 | + let placeholder_color = if query.is_empty() { Color::AnsiValue(243) } else { Color::White }; | |
| 2763 | + execute!( | |
| 2764 | + self.stdout, | |
| 2765 | + MoveTo(start_col as u16, (start_row + 1) as u16), | |
| 2766 | + SetBackgroundColor(bg), | |
| 2767 | + SetForegroundColor(border_color), | |
| 2768 | + Print("│ "), | |
| 2769 | + SetBackgroundColor(input_bg), | |
| 2770 | + SetForegroundColor(placeholder_color), | |
| 2771 | + Print(format!(" {:<width$}", display_query, width = input_display_width)), | |
| 2772 | + SetBackgroundColor(bg), | |
| 2773 | + SetForegroundColor(border_color), | |
| 2774 | + Print(" │"), | |
| 2775 | + ResetColor, | |
| 2776 | + )?; | |
| 2777 | + | |
| 2778 | + // Draw separator | |
| 2779 | + execute!( | |
| 2780 | + self.stdout, | |
| 2781 | + MoveTo(start_col as u16, (start_row + 2) as u16), | |
| 2782 | + SetBackgroundColor(bg), | |
| 2783 | + SetForegroundColor(border_color), | |
| 2784 | + Print(format!("├{:─<width$}┤", "", width = modal_width.saturating_sub(2))), | |
| 2785 | + ResetColor, | |
| 2786 | + )?; | |
| 2787 | + | |
| 2788 | + // Calculate visible range | |
| 2789 | + let visible_rows = modal_height.saturating_sub(5); | |
| 2790 | + | |
| 2791 | + // Adjust scroll offset for visibility | |
| 2792 | + let scroll = if selected_index < scroll_offset { | |
| 2793 | + selected_index | |
| 2794 | + } else if selected_index >= scroll_offset + visible_rows { | |
| 2795 | + selected_index - visible_rows + 1 | |
| 2796 | + } else { | |
| 2797 | + scroll_offset | |
| 2798 | + }; | |
| 2799 | + | |
| 2800 | + // Track current category for headers | |
| 2801 | + let mut last_category = String::new(); | |
| 2802 | + | |
| 2803 | + // Draw keybindings | |
| 2804 | + let mut row_offset = 0; | |
| 2805 | + for (idx, (shortcut, description, category)) in keybinds.iter().enumerate().skip(scroll) { | |
| 2806 | + if row_offset >= visible_rows { | |
| 2807 | + break; | |
| 2808 | + } | |
| 2809 | + | |
| 2810 | + let row = (start_row + 3 + row_offset) as u16; | |
| 2811 | + let is_selected = idx == selected_index; | |
| 2812 | + | |
| 2813 | + // Check if we're starting a new category (only when not filtering) | |
| 2814 | + if query.is_empty() && category != &last_category { | |
| 2815 | + last_category = category.clone(); | |
| 2816 | + } | |
| 2817 | + | |
| 2818 | + let item_bg = if is_selected { selected_bg } else { bg }; | |
| 2819 | + | |
| 2820 | + // Format: Shortcut (fixed width) Description | |
| 2821 | + let shortcut_width = 20; | |
| 2822 | + let desc_width = modal_width.saturating_sub(shortcut_width + 8); | |
| 2823 | + | |
| 2824 | + // Truncate description if needed | |
| 2825 | + let display_desc = if description.len() > desc_width { | |
| 2826 | + format!("{}…", &description[..desc_width.saturating_sub(1)]) | |
| 2827 | + } else { | |
| 2828 | + description.clone() | |
| 2829 | + }; | |
| 2830 | + | |
| 2831 | + // Truncate shortcut if needed | |
| 2832 | + let display_shortcut = if shortcut.len() > shortcut_width { | |
| 2833 | + format!("{}…", &shortcut[..shortcut_width.saturating_sub(1)]) | |
| 2834 | + } else { | |
| 2835 | + shortcut.clone() | |
| 2836 | + }; | |
| 2837 | + | |
| 2838 | + execute!( | |
| 2839 | + self.stdout, | |
| 2840 | + MoveTo(start_col as u16, row), | |
| 2841 | + SetBackgroundColor(item_bg), | |
| 2842 | + SetForegroundColor(border_color), | |
| 2843 | + Print("│ "), | |
| 2844 | + SetForegroundColor(shortcut_color), | |
| 2845 | + SetAttribute(crossterm::style::Attribute::Bold), | |
| 2846 | + Print(format!("{:<width$}", display_shortcut, width = shortcut_width)), | |
| 2847 | + SetAttribute(crossterm::style::Attribute::Reset), | |
| 2848 | + SetBackgroundColor(item_bg), | |
| 2849 | + SetForegroundColor(desc_color), | |
| 2850 | + Print(format!(" {:<width$}", display_desc, width = desc_width)), | |
| 2851 | + SetForegroundColor(category_color), | |
| 2852 | + Print(format!(" {:>6}", if row_offset == 0 || query.is_empty() && keybinds.get(idx.wrapping_sub(1)).map(|(_, _, c)| c != category).unwrap_or(true) { category.as_str() } else { "" })), | |
| 2853 | + SetForegroundColor(border_color), | |
| 2854 | + Print(" │"), | |
| 2855 | + ResetColor, | |
| 2856 | + )?; | |
| 2857 | + | |
| 2858 | + row_offset += 1; | |
| 2859 | + } | |
| 2860 | + | |
| 2861 | + // Fill remaining rows | |
| 2862 | + for i in row_offset..visible_rows { | |
| 2863 | + let row = (start_row + 3 + i) as u16; | |
| 2864 | + execute!( | |
| 2865 | + self.stdout, | |
| 2866 | + MoveTo(start_col as u16, row), | |
| 2867 | + SetBackgroundColor(bg), | |
| 2868 | + SetForegroundColor(border_color), | |
| 2869 | + Print(format!("│{:width$}│", "", width = modal_width.saturating_sub(2))), | |
| 2870 | + ResetColor, | |
| 2871 | + )?; | |
| 2872 | + } | |
| 2873 | + | |
| 2874 | + // Draw info row | |
| 2875 | + let info_row = (start_row + 3 + visible_rows) as u16; | |
| 2876 | + let result_count = if keybinds.is_empty() { | |
| 2877 | + "No matches".to_string() | |
| 2878 | + } else { | |
| 2879 | + format!("{} keybinds", keybinds.len()) | |
| 2880 | + }; | |
| 2881 | + execute!( | |
| 2882 | + self.stdout, | |
| 2883 | + MoveTo(start_col as u16, info_row), | |
| 2884 | + SetBackgroundColor(bg), | |
| 2885 | + SetForegroundColor(border_color), | |
| 2886 | + Print("├"), | |
| 2887 | + SetForegroundColor(Color::AnsiValue(243)), | |
| 2888 | + Print(format!(" {} ", result_count)), | |
| 2889 | + SetForegroundColor(border_color), | |
| 2890 | + Print(format!("{:─<width$}", "", width = modal_width.saturating_sub(result_count.len() + 4))), | |
| 2891 | + Print("┤"), | |
| 2892 | + ResetColor, | |
| 2893 | + )?; | |
| 2894 | + | |
| 2895 | + // Draw bottom border | |
| 2896 | + execute!( | |
| 2897 | + self.stdout, | |
| 2898 | + MoveTo(start_col as u16, info_row + 1), | |
| 2899 | + SetBackgroundColor(bg), | |
| 2900 | + SetForegroundColor(border_color), | |
| 2901 | + Print(format!("╰{:─<width$}╯", "", width = modal_width.saturating_sub(2))), | |
| 2902 | + ResetColor, | |
| 2903 | + )?; | |
| 2904 | + | |
| 2905 | + // Show help text below | |
| 2906 | + let help_text = "↑↓:scroll PgUp/PgDn:page Home/End:jump Esc:close"; | |
| 2907 | + execute!( | |
| 2908 | + self.stdout, | |
| 2909 | + MoveTo(start_col as u16, info_row + 2), | |
| 2910 | + SetForegroundColor(Color::AnsiValue(243)), | |
| 2911 | + Print(format!("{:^width$}", help_text, width = modal_width)), | |
| 2912 | + ResetColor, | |
| 2913 | + )?; | |
| 2914 | + | |
| 2915 | + // Hide cursor when in modal | |
| 2916 | + execute!(self.stdout, Hide)?; | |
| 2917 | + | |
| 2918 | + self.stdout.flush()?; | |
| 2919 | + Ok(()) | |
| 2920 | + } | |
| 2921 | + | |
| 2713 | 2922 | /// Render the LSP references panel (sidebar style) |
| 2714 | 2923 | pub fn render_references_panel( |
| 2715 | 2924 | &mut self, |