working keymap
This commit is contained in:
@@ -39,6 +39,7 @@ validation = ["regex"]
|
|||||||
computed = []
|
computed = []
|
||||||
textarea = ["dep:ropey","gui"]
|
textarea = ["dep:ropey","gui"]
|
||||||
syntect = ["dep:syntect", "gui", "textarea"]
|
syntect = ["dep:syntect", "gui", "textarea"]
|
||||||
|
keymap = ["gui"]
|
||||||
|
|
||||||
# text modes (mutually exclusive; default to vim)
|
# text modes (mutually exclusive; default to vim)
|
||||||
textmode-vim = []
|
textmode-vim = []
|
||||||
@@ -50,7 +51,8 @@ all-nontextmodes = [
|
|||||||
"cursor-style",
|
"cursor-style",
|
||||||
"validation",
|
"validation",
|
||||||
"computed",
|
"computed",
|
||||||
"textarea"
|
"textarea",
|
||||||
|
"keymap"
|
||||||
]
|
]
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
@@ -106,3 +108,8 @@ path = "examples/textarea_normal.rs"
|
|||||||
name = "textarea_syntax"
|
name = "textarea_syntax"
|
||||||
required-features = ["gui", "cursor-style", "textarea", "textmode-normal", "syntect"]
|
required-features = ["gui", "cursor-style", "textarea", "textmode-normal", "syntect"]
|
||||||
path = "examples/textarea_syntax.rs"
|
path = "examples/textarea_syntax.rs"
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "canvas_keymap"
|
||||||
|
required-features = ["gui", "keymap", "cursor-style"]
|
||||||
|
path = "examples/canvas_keymap.rs"
|
||||||
|
|||||||
376
canvas/examples/canvas_keymap.rs
Normal file
376
canvas/examples/canvas_keymap.rs
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
// examples/canvas_keymap.rs
|
||||||
|
//! Demonstrates the centralized keymap system for canvas interactions
|
||||||
|
//!
|
||||||
|
//! This example shows how to use the canvas-keymap feature to delegate
|
||||||
|
//! all canvas key handling to the library, supporting complex sequences
|
||||||
|
//! like "gg", "ge", etc.
|
||||||
|
//!
|
||||||
|
//! Run with:
|
||||||
|
//! cargo run --example canvas_keymap --features "gui,keymap,cursor-style"
|
||||||
|
|
||||||
|
#[cfg(not(feature = "keymap"))]
|
||||||
|
compile_error!(
|
||||||
|
"This example requires the 'keymap' feature. \
|
||||||
|
Run with: cargo run --example canvas_keymap --features \"gui,keymap,cursor-style\""
|
||||||
|
);
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io;
|
||||||
|
use crossterm::{
|
||||||
|
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyEvent},
|
||||||
|
execute,
|
||||||
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
|
};
|
||||||
|
use ratatui::{
|
||||||
|
backend::{Backend, CrosstermBackend},
|
||||||
|
layout::{Constraint, Direction, Layout},
|
||||||
|
style::{Color, Style},
|
||||||
|
text::{Line, Span},
|
||||||
|
widgets::{Block, Borders, Paragraph},
|
||||||
|
Frame, Terminal,
|
||||||
|
};
|
||||||
|
|
||||||
|
use canvas::{
|
||||||
|
canvas::{gui::render_canvas_default, modes::AppMode},
|
||||||
|
keymap::{CanvasKeyMap, KeyEventOutcome},
|
||||||
|
DataProvider, FormEditor,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Demo application using centralized keymap system
|
||||||
|
struct KeymapDemoApp {
|
||||||
|
editor: FormEditor<DemoData>,
|
||||||
|
message: String,
|
||||||
|
quit: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeymapDemoApp {
|
||||||
|
fn new() -> Self {
|
||||||
|
let data = DemoData::new();
|
||||||
|
let mut editor = FormEditor::new(data);
|
||||||
|
|
||||||
|
// Build and inject the keymap from our config
|
||||||
|
let keymap = Self::build_demo_keymap();
|
||||||
|
editor.set_keymap(keymap);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
editor,
|
||||||
|
message: "🎯 Keymap system loaded! Try: gg, ge, hjkl, w/b/e, v, i, etc.".to_string(),
|
||||||
|
quit: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a comprehensive keymap configuration
|
||||||
|
fn build_demo_keymap() -> CanvasKeyMap {
|
||||||
|
let mut read_only = HashMap::new();
|
||||||
|
let mut edit = HashMap::new();
|
||||||
|
let mut highlight = HashMap::new();
|
||||||
|
|
||||||
|
// === READ-ONLY MODE KEYBINDINGS ===
|
||||||
|
|
||||||
|
// Basic movement
|
||||||
|
read_only.insert("move_left".to_string(), vec!["h".to_string(), "Left".to_string()]);
|
||||||
|
read_only.insert("move_right".to_string(), vec!["l".to_string(), "Right".to_string()]);
|
||||||
|
read_only.insert("move_up".to_string(), vec!["k".to_string(), "Up".to_string()]);
|
||||||
|
read_only.insert("move_down".to_string(), vec!["j".to_string(), "Down".to_string()]);
|
||||||
|
|
||||||
|
// Word movement
|
||||||
|
read_only.insert("move_word_next".to_string(), vec!["w".to_string()]);
|
||||||
|
read_only.insert("move_word_prev".to_string(), vec!["b".to_string()]);
|
||||||
|
read_only.insert("move_word_end".to_string(), vec!["e".to_string()]);
|
||||||
|
read_only.insert("move_word_end_prev".to_string(), vec!["ge".to_string()]); // Multi-key!
|
||||||
|
|
||||||
|
// Big word movement
|
||||||
|
read_only.insert("move_big_word_next".to_string(), vec!["W".to_string()]);
|
||||||
|
read_only.insert("move_big_word_prev".to_string(), vec!["B".to_string()]);
|
||||||
|
read_only.insert("move_big_word_end".to_string(), vec!["E".to_string()]);
|
||||||
|
read_only.insert("move_big_word_end_prev".to_string(), vec!["gE".to_string()]); // Multi-key!
|
||||||
|
|
||||||
|
// Line movement
|
||||||
|
read_only.insert("move_line_start".to_string(), vec!["0".to_string(), "Home".to_string()]);
|
||||||
|
read_only.insert("move_line_end".to_string(), vec!["$".to_string(), "End".to_string()]);
|
||||||
|
|
||||||
|
// Field movement
|
||||||
|
read_only.insert("move_first_line".to_string(), vec!["gg".to_string()]); // Multi-key!
|
||||||
|
read_only.insert("move_last_line".to_string(), vec!["G".to_string()]);
|
||||||
|
read_only.insert("next_field".to_string(), vec!["Tab".to_string()]);
|
||||||
|
read_only.insert("prev_field".to_string(), vec!["Shift+Tab".to_string()]);
|
||||||
|
|
||||||
|
// Mode transitions
|
||||||
|
read_only.insert("enter_edit_mode_before".to_string(), vec!["i".to_string()]);
|
||||||
|
read_only.insert("enter_edit_mode_after".to_string(), vec!["a".to_string()]);
|
||||||
|
read_only.insert("enter_highlight_mode".to_string(), vec!["v".to_string()]);
|
||||||
|
read_only.insert("enter_highlight_mode_linewise".to_string(), vec!["V".to_string()]);
|
||||||
|
|
||||||
|
// Editing actions in normal mode
|
||||||
|
read_only.insert("delete_char_forward".to_string(), vec!["x".to_string()]);
|
||||||
|
read_only.insert("delete_char_backward".to_string(), vec!["X".to_string()]);
|
||||||
|
read_only.insert("open_line_below".to_string(), vec!["o".to_string()]);
|
||||||
|
read_only.insert("open_line_above".to_string(), vec!["O".to_string()]);
|
||||||
|
|
||||||
|
// === EDIT MODE KEYBINDINGS ===
|
||||||
|
|
||||||
|
edit.insert("exit_edit_mode".to_string(), vec!["esc".to_string()]);
|
||||||
|
edit.insert("move_left".to_string(), vec!["Left".to_string()]);
|
||||||
|
edit.insert("move_right".to_string(), vec!["Right".to_string()]);
|
||||||
|
edit.insert("move_up".to_string(), vec!["Up".to_string()]);
|
||||||
|
edit.insert("move_down".to_string(), vec!["Down".to_string()]);
|
||||||
|
edit.insert("move_line_start".to_string(), vec!["Home".to_string()]);
|
||||||
|
edit.insert("move_line_end".to_string(), vec!["End".to_string()]);
|
||||||
|
edit.insert("move_word_next".to_string(), vec!["Ctrl+Right".to_string()]);
|
||||||
|
edit.insert("move_word_prev".to_string(), vec!["Ctrl+Left".to_string()]);
|
||||||
|
edit.insert("next_field".to_string(), vec!["Tab".to_string()]);
|
||||||
|
edit.insert("prev_field".to_string(), vec!["Shift+Tab".to_string()]);
|
||||||
|
edit.insert("delete_char_backward".to_string(), vec!["Backspace".to_string()]);
|
||||||
|
edit.insert("delete_char_forward".to_string(), vec!["Delete".to_string()]);
|
||||||
|
|
||||||
|
// === HIGHLIGHT MODE KEYBINDINGS ===
|
||||||
|
|
||||||
|
highlight.insert("exit_highlight_mode".to_string(), vec!["esc".to_string()]);
|
||||||
|
highlight.insert("enter_highlight_mode_linewise".to_string(), vec!["V".to_string()]);
|
||||||
|
|
||||||
|
// Movement (extends selection)
|
||||||
|
highlight.insert("move_left".to_string(), vec!["h".to_string(), "Left".to_string()]);
|
||||||
|
highlight.insert("move_right".to_string(), vec!["l".to_string(), "Right".to_string()]);
|
||||||
|
highlight.insert("move_up".to_string(), vec!["k".to_string(), "Up".to_string()]);
|
||||||
|
highlight.insert("move_down".to_string(), vec!["j".to_string(), "Down".to_string()]);
|
||||||
|
highlight.insert("move_word_next".to_string(), vec!["w".to_string()]);
|
||||||
|
highlight.insert("move_word_prev".to_string(), vec!["b".to_string()]);
|
||||||
|
highlight.insert("move_word_end".to_string(), vec!["e".to_string()]);
|
||||||
|
highlight.insert("move_word_end_prev".to_string(), vec!["ge".to_string()]);
|
||||||
|
highlight.insert("move_line_start".to_string(), vec!["0".to_string()]);
|
||||||
|
highlight.insert("move_line_end".to_string(), vec!["$".to_string()]);
|
||||||
|
highlight.insert("move_first_line".to_string(), vec!["gg".to_string()]);
|
||||||
|
highlight.insert("move_last_line".to_string(), vec!["G".to_string()]);
|
||||||
|
|
||||||
|
CanvasKeyMap::from_mode_maps(&read_only, &edit, &highlight)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_key_event(&mut self, key_event: KeyEvent) -> io::Result<()> {
|
||||||
|
// First, try canvas keymap
|
||||||
|
match self.editor.handle_key_event(key_event) {
|
||||||
|
KeyEventOutcome::Consumed(Some(msg)) => {
|
||||||
|
self.message = format!("🎯 Canvas: {}", msg);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
KeyEventOutcome::Consumed(None) => {
|
||||||
|
self.message = "🎯 Canvas action executed".to_string();
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
KeyEventOutcome::Pending => {
|
||||||
|
self.message = "⏳ Waiting for next key in sequence...".to_string();
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
KeyEventOutcome::NotMatched => {
|
||||||
|
// Fall through to client actions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle client-specific actions (non-canvas)
|
||||||
|
use crossterm::event::{KeyCode, KeyModifiers};
|
||||||
|
match (key_event.code, key_event.modifiers) {
|
||||||
|
(KeyCode::Char('q'), KeyModifiers::CONTROL) |
|
||||||
|
(KeyCode::Char('c'), KeyModifiers::CONTROL) => {
|
||||||
|
self.quit = true;
|
||||||
|
self.message = "👋 Goodbye!".to_string();
|
||||||
|
}
|
||||||
|
(KeyCode::F(1), _) => {
|
||||||
|
self.message = "ℹ️ F1: This is a client action (not handled by canvas keymap)".to_string();
|
||||||
|
}
|
||||||
|
(KeyCode::F(2), _) => {
|
||||||
|
// Demonstrate saving
|
||||||
|
self.message = "💾 F2: Save action (client-side)".to_string();
|
||||||
|
}
|
||||||
|
(KeyCode::Char('?'), _) if self.editor.mode() == AppMode::ReadOnly => {
|
||||||
|
self.show_help();
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Unknown key
|
||||||
|
self.message = format!(
|
||||||
|
"❓ Unhandled key: {:?} (mode: {:?})",
|
||||||
|
key_event.code,
|
||||||
|
self.editor.mode()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_help(&mut self) {
|
||||||
|
self.message = "📖 Help: Multi-key sequences work! Try gg, ge, gE. Also: hjkl, w/b/e, v/V, i/a/o".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_quit(&self) -> bool {
|
||||||
|
self.quit
|
||||||
|
}
|
||||||
|
|
||||||
|
fn editor(&self) -> &FormEditor<DemoData> {
|
||||||
|
&self.editor
|
||||||
|
}
|
||||||
|
|
||||||
|
fn message(&self) -> &str {
|
||||||
|
&self.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Demo form data with interesting examples for keymap testing
|
||||||
|
struct DemoData {
|
||||||
|
fields: Vec<(String, String)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DemoData {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
fields: vec![
|
||||||
|
("🎯 Name".to_string(), "John-Paul McDonald-Smith".to_string()),
|
||||||
|
("📧 Email".to_string(), "user@long-domain-name.example.com".to_string()),
|
||||||
|
("📱 Phone".to_string(), "+1 (555) 123-4567 ext. 890".to_string()),
|
||||||
|
("🏠 Address".to_string(), "123 Main Street, Apartment 4B, Suite 100".to_string()),
|
||||||
|
("🏷️ Tags".to_string(), "urgent,important,follow-up,high-priority".to_string()),
|
||||||
|
("📝 Notes".to_string(), "Test word movements: w=next-word, b=prev-word, e=word-end, ge=prev-word-end".to_string()),
|
||||||
|
("🔥 Multi-key".to_string(), "Try multi-key sequences: gg=first-field, ge=prev-word-end, gE=prev-WORD-end".to_string()),
|
||||||
|
("⚡ Vim Actions".to_string(), "Normal mode: x=delete-char, o=open-line-below, v=visual, i=insert".to_string()),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DataProvider for DemoData {
|
||||||
|
fn field_count(&self) -> usize {
|
||||||
|
self.fields.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn field_name(&self, index: usize) -> &str {
|
||||||
|
&self.fields[index].0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn field_value(&self, index: usize) -> &str {
|
||||||
|
&self.fields[index].1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_field_value(&mut self, index: usize, value: String) {
|
||||||
|
self.fields[index].1 = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: KeymapDemoApp) -> io::Result<()> {
|
||||||
|
loop {
|
||||||
|
terminal.draw(|f| ui(f, &app))?;
|
||||||
|
|
||||||
|
if let Event::Key(key) = event::read()? {
|
||||||
|
app.handle_key_event(key)?;
|
||||||
|
if app.should_quit() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ui(f: &mut Frame, app: &KeymapDemoApp) {
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Min(8), Constraint::Length(12)])
|
||||||
|
.split(f.area());
|
||||||
|
|
||||||
|
// Render the canvas
|
||||||
|
render_canvas_default(f, chunks[0], app.editor());
|
||||||
|
|
||||||
|
// Render status and help
|
||||||
|
render_status_and_help(f, chunks[1], app);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_status_and_help(f: &mut Frame, area: ratatui::layout::Rect, app: &KeymapDemoApp) {
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Length(3), Constraint::Min(9)])
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
// Status message
|
||||||
|
let status_text = format!(
|
||||||
|
"Mode: {:?} | Field: {}/{} | Pos: {} | {}",
|
||||||
|
app.editor().mode(),
|
||||||
|
app.editor().current_field() + 1,
|
||||||
|
app.editor().data_provider().field_count(),
|
||||||
|
app.editor().cursor_position(),
|
||||||
|
app.message()
|
||||||
|
);
|
||||||
|
|
||||||
|
let status = Paragraph::new(Line::from(Span::raw(status_text)))
|
||||||
|
.block(Block::default().borders(Borders::ALL).title("🎯 Keymap Demo Status"));
|
||||||
|
|
||||||
|
f.render_widget(status, chunks[0]);
|
||||||
|
|
||||||
|
// Help text based on current mode
|
||||||
|
let help_text = match app.editor().mode() {
|
||||||
|
AppMode::ReadOnly => {
|
||||||
|
"🎯 KEYMAP DEMO - All keys handled by centralized keymap system!\n\
|
||||||
|
\n\
|
||||||
|
📍 MOVEMENT: hjkl(basic) | w/b/e(words) | W/B/E(WORDS) | 0/$(line) | gg/G(fields)\n\
|
||||||
|
🔥 MULTI-KEY: gg=first-field, ge=prev-word-end, gE=prev-WORD-end\n\
|
||||||
|
✏️ MODES: i/a(insert) | v/V(visual) | o/O(open-line)\n\
|
||||||
|
🗑️ DELETE: x/X(delete-char)\n\
|
||||||
|
📂 FIELDS: Tab/Shift+Tab\n\
|
||||||
|
\n\
|
||||||
|
💡 Try multi-key sequences like 'gg' or 'ge' - watch the status for 'Waiting...'\n\
|
||||||
|
🚪 Ctrl+C=quit | ?=help | F1/F2=client actions (not canvas)"
|
||||||
|
}
|
||||||
|
AppMode::Edit => {
|
||||||
|
"✏️ INSERT MODE - Keys handled by keymap system\n\
|
||||||
|
\n\
|
||||||
|
🔄 NAVIGATION: arrows | Ctrl+arrows(words) | Home/End(line) | Tab/Shift+Tab(fields)\n\
|
||||||
|
🗑️ DELETE: Backspace/Delete\n\
|
||||||
|
🚪 EXIT: Esc=normal\n\
|
||||||
|
\n\
|
||||||
|
💡 Type text normally - the keymap handles navigation!"
|
||||||
|
}
|
||||||
|
AppMode::Highlight => {
|
||||||
|
"🎯 VISUAL MODE - Selection extended by keymap movements\n\
|
||||||
|
\n\
|
||||||
|
📍 EXTEND: hjkl(basic) | w/b/e(words) | 0/$(line) | gg/G(fields)\n\
|
||||||
|
🔄 SWITCH: V=toggle-line-mode\n\
|
||||||
|
🚪 EXIT: Esc=normal\n\
|
||||||
|
\n\
|
||||||
|
💡 All movements extend the selection automatically!"
|
||||||
|
}
|
||||||
|
_ => "🎯 Keymap system active!"
|
||||||
|
};
|
||||||
|
|
||||||
|
let help = Paragraph::new(help_text)
|
||||||
|
.block(Block::default().borders(Borders::ALL).title("🚀 Centralized Keymap System"))
|
||||||
|
.style(Style::default().fg(Color::Gray));
|
||||||
|
|
||||||
|
f.render_widget(help, chunks[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("🎯 Canvas Keymap Demo");
|
||||||
|
println!("✅ canvas-keymap feature: ENABLED");
|
||||||
|
println!("🚀 Centralized key handling: ACTIVE");
|
||||||
|
println!("📖 Multi-key sequences: SUPPORTED (gg, ge, gE, etc.)");
|
||||||
|
println!();
|
||||||
|
|
||||||
|
enable_raw_mode()?;
|
||||||
|
let mut stdout = io::stdout();
|
||||||
|
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||||
|
let backend = CrosstermBackend::new(stdout);
|
||||||
|
let mut terminal = Terminal::new(backend)?;
|
||||||
|
|
||||||
|
let app = KeymapDemoApp::new();
|
||||||
|
let res = run_app(&mut terminal, app);
|
||||||
|
|
||||||
|
disable_raw_mode()?;
|
||||||
|
execute!(
|
||||||
|
terminal.backend_mut(),
|
||||||
|
LeaveAlternateScreen,
|
||||||
|
DisableMouseCapture
|
||||||
|
)?;
|
||||||
|
terminal.show_cursor()?;
|
||||||
|
|
||||||
|
if let Err(err) = res {
|
||||||
|
println!("{err:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("🎯 Keymap demo completed!");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -9,6 +9,10 @@ use crate::DataProvider;
|
|||||||
#[cfg(feature = "suggestions")]
|
#[cfg(feature = "suggestions")]
|
||||||
use crate::SuggestionItem;
|
use crate::SuggestionItem;
|
||||||
|
|
||||||
|
// NEW: Import keymap types when keymap feature is enabled
|
||||||
|
#[cfg(feature = "keymap")]
|
||||||
|
use crate::keymap::{CanvasKeyMap, KeySequenceTracker};
|
||||||
|
|
||||||
pub struct FormEditor<D: DataProvider> {
|
pub struct FormEditor<D: DataProvider> {
|
||||||
pub(crate) ui_state: EditorState,
|
pub(crate) ui_state: EditorState,
|
||||||
pub(crate) data_provider: D,
|
pub(crate) data_provider: D,
|
||||||
@@ -23,6 +27,12 @@ pub struct FormEditor<D: DataProvider> {
|
|||||||
+ Sync,
|
+ Sync,
|
||||||
>,
|
>,
|
||||||
>,
|
>,
|
||||||
|
|
||||||
|
// NEW: Injected keymap and sequence tracker (keymap feature only)
|
||||||
|
#[cfg(feature = "keymap")]
|
||||||
|
pub(crate) keymap: Option<CanvasKeyMap>,
|
||||||
|
#[cfg(feature = "keymap")]
|
||||||
|
pub(crate) seq_tracker: KeySequenceTracker,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<D: DataProvider> FormEditor<D> {
|
impl<D: DataProvider> FormEditor<D> {
|
||||||
@@ -47,6 +57,11 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
suggestions: Vec::new(),
|
suggestions: Vec::new(),
|
||||||
#[cfg(feature = "validation")]
|
#[cfg(feature = "validation")]
|
||||||
external_validation_callback: None,
|
external_validation_callback: None,
|
||||||
|
// NEW: Initialize keymap fields
|
||||||
|
#[cfg(feature = "keymap")]
|
||||||
|
keymap: None,
|
||||||
|
#[cfg(feature = "keymap")]
|
||||||
|
seq_tracker: KeySequenceTracker::new(400), // 400ms default timeout
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "validation")]
|
#[cfg(feature = "validation")]
|
||||||
@@ -70,6 +85,26 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NEW: Keymap management methods (keymap feature only)
|
||||||
|
|
||||||
|
/// Set the keymap for this editor instance
|
||||||
|
#[cfg(feature = "keymap")]
|
||||||
|
pub fn set_keymap(&mut self, keymap: CanvasKeyMap) {
|
||||||
|
self.keymap = Some(keymap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if this editor has a keymap configured
|
||||||
|
#[cfg(feature = "keymap")]
|
||||||
|
pub fn has_keymap(&self) -> bool {
|
||||||
|
self.keymap.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the timeout for multi-key sequences (in milliseconds)
|
||||||
|
#[cfg(feature = "keymap")]
|
||||||
|
pub fn set_key_sequence_timeout_ms(&mut self, timeout_ms: u64) {
|
||||||
|
self.seq_tracker = KeySequenceTracker::new(timeout_ms);
|
||||||
|
}
|
||||||
|
|
||||||
// Library-internal, used by multiple modules
|
// Library-internal, used by multiple modules
|
||||||
pub(crate) fn current_text(&self) -> &str {
|
pub(crate) fn current_text(&self) -> &str {
|
||||||
let field_index = self.ui_state.current_field;
|
let field_index = self.ui_state.current_field;
|
||||||
|
|||||||
228
canvas/src/editor/key_input.rs
Normal file
228
canvas/src/editor/key_input.rs
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
// src/editor/key_input.rs
|
||||||
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
|
|
||||||
|
use crate::canvas::modes::AppMode;
|
||||||
|
use crate::editor::FormEditor;
|
||||||
|
use crate::DataProvider;
|
||||||
|
|
||||||
|
#[cfg(feature = "keymap")]
|
||||||
|
use crate::keymap::{KeyEventOutcome, KeyStroke};
|
||||||
|
|
||||||
|
impl<D: DataProvider> FormEditor<D> {
|
||||||
|
#[cfg(feature = "keymap")]
|
||||||
|
pub fn handle_key_event(&mut self, evt: KeyEvent) -> KeyEventOutcome {
|
||||||
|
// Check if keymap exists first
|
||||||
|
if self.keymap.is_none() {
|
||||||
|
return KeyEventOutcome::NotMatched;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mode = self.ui_state.current_mode;
|
||||||
|
|
||||||
|
// Convert event to normalized stroke
|
||||||
|
let stroke = KeyStroke {
|
||||||
|
code: evt.code,
|
||||||
|
modifiers: evt.modifiers,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add key to sequence tracker
|
||||||
|
self.seq_tracker.add_key(stroke);
|
||||||
|
|
||||||
|
// Look up the action in keymap
|
||||||
|
let (matched, is_prefix) = {
|
||||||
|
let km = self.keymap.as_ref().unwrap();
|
||||||
|
km.lookup(mode, self.seq_tracker.sequence())
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(action) = matched {
|
||||||
|
// Clone the action string to avoid borrow checker issues
|
||||||
|
let action_owned = action.to_string();
|
||||||
|
let msg = self.dispatch_canvas_action(&action_owned);
|
||||||
|
self.seq_tracker.reset();
|
||||||
|
return KeyEventOutcome::Consumed(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_prefix {
|
||||||
|
// Wait for more keys
|
||||||
|
return KeyEventOutcome::Pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No match: reset sequence and try insert-char fallback in Edit
|
||||||
|
self.seq_tracker.reset();
|
||||||
|
|
||||||
|
if mode == AppMode::Edit {
|
||||||
|
if let KeyCode::Char(c) = evt.code {
|
||||||
|
// Skip control/alt combos
|
||||||
|
let m = evt.modifiers;
|
||||||
|
let is_plain =
|
||||||
|
m.is_empty() || m == KeyModifiers::SHIFT;
|
||||||
|
if is_plain {
|
||||||
|
if self.insert_char(c).is_ok() {
|
||||||
|
return KeyEventOutcome::Consumed(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyEventOutcome::NotMatched
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "keymap")]
|
||||||
|
fn dispatch_canvas_action(&mut self, action: &str) -> Option<String> {
|
||||||
|
match action {
|
||||||
|
// Movement
|
||||||
|
"move_left" => {
|
||||||
|
let _ = self.move_left();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
"move_right" => {
|
||||||
|
let _ = self.move_right();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
"move_up" => {
|
||||||
|
let _ = self.move_up();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
"move_down" => {
|
||||||
|
let _ = self.move_down();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
"next_field" => {
|
||||||
|
let _ = self.next_field();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
"prev_field" => {
|
||||||
|
let _ = self.prev_field();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
"move_line_start" => {
|
||||||
|
self.move_line_start();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
"move_line_end" => {
|
||||||
|
self.move_line_end();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
"move_first_line" => {
|
||||||
|
let _ = self.move_first_line();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
"move_last_line" => {
|
||||||
|
let _ = self.move_last_line();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
// Word/big-word movement (cross-field aware)
|
||||||
|
"move_word_next" => {
|
||||||
|
self.move_word_next();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
"move_word_prev" => {
|
||||||
|
self.move_word_prev();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
"move_word_end" => {
|
||||||
|
self.move_word_end();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
"move_word_end_prev" => {
|
||||||
|
self.move_word_end_prev();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
"move_big_word_next" => {
|
||||||
|
self.move_big_word_next();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
"move_big_word_prev" => {
|
||||||
|
self.move_big_word_prev();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
"move_big_word_end" => {
|
||||||
|
self.move_big_word_end();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
"move_big_word_end_prev" => {
|
||||||
|
self.move_big_word_end_prev();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
// Editing
|
||||||
|
"delete_char_backward" => {
|
||||||
|
let _ = self.delete_backward();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
"delete_char_forward" => {
|
||||||
|
let _ = self.delete_forward();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
"open_line_below" => {
|
||||||
|
let _ = self.open_line_below();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
"open_line_above" => {
|
||||||
|
let _ = self.open_line_above();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suggestions (only when feature is enabled)
|
||||||
|
#[cfg(feature = "suggestions")]
|
||||||
|
"open_suggestions" => {
|
||||||
|
let idx = self.current_field();
|
||||||
|
self.open_suggestions(idx);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
#[cfg(feature = "suggestions")]
|
||||||
|
"apply_suggestion" | "enter_decider" => {
|
||||||
|
if let Some(_applied) = self.apply_suggestion() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(feature = "suggestions")]
|
||||||
|
"suggestion_down" => {
|
||||||
|
self.suggestions_next();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
#[cfg(feature = "suggestions")]
|
||||||
|
"suggestion_up" => {
|
||||||
|
self.suggestions_prev();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode transitions (vim-like)
|
||||||
|
"enter_edit_mode_before" => {
|
||||||
|
self.enter_edit_mode();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
"enter_edit_mode_after" => {
|
||||||
|
// Move forward 1 char if possible (vim 'a'), then enter insert
|
||||||
|
let txt_len = self.current_text().chars().count();
|
||||||
|
let pos = self.ui_state.cursor_pos;
|
||||||
|
if pos < txt_len {
|
||||||
|
self.ui_state.cursor_pos = pos + 1;
|
||||||
|
self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos;
|
||||||
|
}
|
||||||
|
self.enter_edit_mode();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
"exit" | "exit_edit_mode" => {
|
||||||
|
let _ = self.exit_edit_mode();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
"enter_highlight_mode" => {
|
||||||
|
self.enter_highlight_mode();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
"enter_highlight_mode_linewise" => {
|
||||||
|
self.enter_highlight_line_mode();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
"exit_highlight_mode" => {
|
||||||
|
self.exit_highlight_mode();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,5 +21,8 @@ pub mod validation_helpers;
|
|||||||
#[cfg(feature = "computed")]
|
#[cfg(feature = "computed")]
|
||||||
pub mod computed_helpers;
|
pub mod computed_helpers;
|
||||||
|
|
||||||
|
#[cfg(feature = "keymap")]
|
||||||
|
pub mod key_input;
|
||||||
|
|
||||||
// Re-export the main type
|
// Re-export the main type
|
||||||
pub use core::FormEditor;
|
pub use core::FormEditor;
|
||||||
|
|||||||
344
canvas/src/keymap/mod.rs
Normal file
344
canvas/src/keymap/mod.rs
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
// src/keymap/mod.rs
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use crossterm::event::{KeyCode, KeyModifiers};
|
||||||
|
|
||||||
|
use crate::canvas::modes::AppMode;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub struct KeyStroke {
|
||||||
|
pub code: KeyCode,
|
||||||
|
pub modifiers: KeyModifiers,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct Binding {
|
||||||
|
action: String,
|
||||||
|
sequence: Vec<KeyStroke>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct CanvasKeyMap {
|
||||||
|
ro: Vec<Binding>,
|
||||||
|
edit: Vec<Binding>,
|
||||||
|
hl: Vec<Binding>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXED: Removed Copy because Option<String> is not Copy
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum KeyEventOutcome {
|
||||||
|
Consumed(Option<String>),
|
||||||
|
Pending,
|
||||||
|
NotMatched,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct KeySequenceTracker {
|
||||||
|
sequence: Vec<KeyStroke>,
|
||||||
|
last_key_time: Instant,
|
||||||
|
timeout: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeySequenceTracker {
|
||||||
|
pub fn new(timeout_ms: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
sequence: Vec::new(),
|
||||||
|
last_key_time: Instant::now(),
|
||||||
|
timeout: Duration::from_millis(timeout_ms),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.sequence.clear();
|
||||||
|
self.last_key_time = Instant::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_key(&mut self, stroke: KeyStroke) {
|
||||||
|
let now = Instant::now();
|
||||||
|
if now.duration_since(self.last_key_time) > self.timeout {
|
||||||
|
self.reset();
|
||||||
|
}
|
||||||
|
self.sequence.push(normalize_stroke(stroke));
|
||||||
|
self.last_key_time = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sequence(&self) -> &[KeyStroke] {
|
||||||
|
&self.sequence
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_stroke(mut s: KeyStroke) -> KeyStroke {
|
||||||
|
// Normalize Shift+Tab to BackTab
|
||||||
|
let is_shift_tab =
|
||||||
|
s.code == KeyCode::Tab && s.modifiers.contains(KeyModifiers::SHIFT);
|
||||||
|
if is_shift_tab {
|
||||||
|
s.code = KeyCode::BackTab;
|
||||||
|
s.modifiers.remove(KeyModifiers::SHIFT);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize Shift+char to uppercase char without SHIFT when possible
|
||||||
|
if let KeyCode::Char(c) = s.code {
|
||||||
|
if s.modifiers.contains(KeyModifiers::SHIFT) {
|
||||||
|
let mut up = c;
|
||||||
|
// Only letters transform meaningfully
|
||||||
|
if c.is_ascii_alphabetic() {
|
||||||
|
up = c.to_ascii_uppercase();
|
||||||
|
}
|
||||||
|
s.code = KeyCode::Char(up);
|
||||||
|
s.modifiers.remove(KeyModifiers::SHIFT);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CanvasKeyMap {
|
||||||
|
pub fn from_mode_maps(
|
||||||
|
read_only: &HashMap<String, Vec<String>>,
|
||||||
|
edit: &HashMap<String, Vec<String>>,
|
||||||
|
highlight: &HashMap<String, Vec<String>>,
|
||||||
|
) -> Self {
|
||||||
|
let mut km = Self::default();
|
||||||
|
km.ro = collect_bindings(read_only);
|
||||||
|
km.edit = collect_bindings(edit);
|
||||||
|
km.hl = collect_bindings(highlight);
|
||||||
|
km
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lookup(
|
||||||
|
&self,
|
||||||
|
mode: AppMode,
|
||||||
|
seq: &[KeyStroke],
|
||||||
|
) -> (Option<&str>, bool) {
|
||||||
|
let bindings = match mode {
|
||||||
|
AppMode::ReadOnly => &self.ro,
|
||||||
|
AppMode::Edit => &self.edit,
|
||||||
|
AppMode::Highlight => &self.hl,
|
||||||
|
_ => return (None, false),
|
||||||
|
};
|
||||||
|
|
||||||
|
if seq.is_empty() {
|
||||||
|
return (None, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exact match
|
||||||
|
for b in bindings {
|
||||||
|
if sequences_equal(&b.sequence, seq) {
|
||||||
|
return (Some(b.action.as_str()), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefix match
|
||||||
|
for b in bindings {
|
||||||
|
if is_prefix(&b.sequence, seq) {
|
||||||
|
return (None, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(None, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sequences_equal(a: &[KeyStroke], b: &[KeyStroke]) -> bool {
|
||||||
|
if a.len() != b.len() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
a.iter().zip(b.iter()).all(|(x, y)| strokes_equal(x, y))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn strokes_equal(a: &KeyStroke, b: &KeyStroke) -> bool {
|
||||||
|
// Both KeyStroke are already normalized
|
||||||
|
a.code == b.code && a.modifiers == b.modifiers
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_prefix(binding: &[KeyStroke], seq: &[KeyStroke]) -> bool {
|
||||||
|
if seq.len() >= binding.len() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
binding
|
||||||
|
.iter()
|
||||||
|
.zip(seq.iter())
|
||||||
|
.all(|(b, s)| strokes_equal(b, s))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_bindings(
|
||||||
|
mode_map: &HashMap<String, Vec<String>>,
|
||||||
|
) -> Vec<Binding> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for (action, list) in mode_map {
|
||||||
|
for binding_str in list {
|
||||||
|
if let Some(seq) = parse_binding_to_sequence(binding_str) {
|
||||||
|
out.push(Binding {
|
||||||
|
action: action.to_string(),
|
||||||
|
sequence: seq,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_binding_to_sequence(input: &str) -> Option<Vec<KeyStroke>> {
|
||||||
|
let s = input.trim();
|
||||||
|
if s.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let has_space = s.contains(' ');
|
||||||
|
let has_plus = s.contains('+');
|
||||||
|
|
||||||
|
if has_space {
|
||||||
|
let mut seq = Vec::new();
|
||||||
|
for part in s.split_whitespace() {
|
||||||
|
if let Some(mut strokes) = parse_part_to_sequence(part) {
|
||||||
|
seq.append(&mut strokes);
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Some(seq);
|
||||||
|
}
|
||||||
|
|
||||||
|
if has_plus {
|
||||||
|
if contains_modifier_token(s) {
|
||||||
|
if let Some(k) = parse_chord_with_modifiers(s) {
|
||||||
|
return Some(vec![k]);
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
} else {
|
||||||
|
let mut seq = Vec::new();
|
||||||
|
for t in s.split('+') {
|
||||||
|
if let Some(mut strokes) = parse_part_to_sequence(t) {
|
||||||
|
seq.append(&mut strokes);
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Some(seq);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_compound_key(s) {
|
||||||
|
if let Some(k) = parse_simple_key(s) {
|
||||||
|
return Some(vec![k]);
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.len() > 1 {
|
||||||
|
let mut seq = Vec::new();
|
||||||
|
for ch in s.chars() {
|
||||||
|
seq.push(KeyStroke {
|
||||||
|
code: KeyCode::Char(ch),
|
||||||
|
modifiers: KeyModifiers::empty(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Some(seq);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(k) = parse_simple_key(s) {
|
||||||
|
return Some(vec![k]);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_part_to_sequence(part: &str) -> Option<Vec<KeyStroke>> {
|
||||||
|
let p = part.trim();
|
||||||
|
if p.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.contains('+') && contains_modifier_token(p) {
|
||||||
|
if let Some(k) = parse_chord_with_modifiers(p) {
|
||||||
|
return Some(vec![k]);
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_compound_key(p) {
|
||||||
|
if let Some(k) = parse_simple_key(p) {
|
||||||
|
return Some(vec![k]);
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.len() > 1 {
|
||||||
|
let mut seq = Vec::new();
|
||||||
|
for ch in p.chars() {
|
||||||
|
seq.push(KeyStroke {
|
||||||
|
code: KeyCode::Char(ch),
|
||||||
|
modifiers: KeyModifiers::empty(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Some(seq);
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_simple_key(p).map(|k| vec![k])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contains_modifier_token(s: &str) -> bool {
|
||||||
|
let low = s.to_lowercase();
|
||||||
|
low.contains("ctrl") || low.contains("shift") || low.contains("alt") ||
|
||||||
|
low.contains("super") || low.contains("cmd") || low.contains("meta")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_chord_with_modifiers(s: &str) -> Option<KeyStroke> {
|
||||||
|
let mut mods = KeyModifiers::empty();
|
||||||
|
let mut key: Option<KeyCode> = None;
|
||||||
|
|
||||||
|
for comp in s.split('+') {
|
||||||
|
match comp.to_lowercase().as_str() {
|
||||||
|
"ctrl" => mods |= KeyModifiers::CONTROL,
|
||||||
|
"shift" => mods |= KeyModifiers::SHIFT,
|
||||||
|
"alt" => mods |= KeyModifiers::ALT,
|
||||||
|
"super" | "cmd" => mods |= KeyModifiers::SUPER,
|
||||||
|
"meta" => mods |= KeyModifiers::META,
|
||||||
|
other => {
|
||||||
|
key = string_to_keycode(other);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
key.map(|k| normalize_stroke(KeyStroke { code: k, modifiers: mods }))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_compound_key(s: &str) -> bool {
|
||||||
|
matches!(s.to_lowercase().as_str(),
|
||||||
|
"left" | "right" | "up" | "down" | "esc" | "enter" | "backspace" |
|
||||||
|
"delete" | "tab" | "home" | "end" | "$" | "0"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_simple_key(s: &str) -> Option<KeyStroke> {
|
||||||
|
if let Some(kc) = string_to_keycode(&s.to_lowercase()) {
|
||||||
|
return Some(KeyStroke { code: kc, modifiers: KeyModifiers::empty() });
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.chars().count() == 1 {
|
||||||
|
let ch = s.chars().next().unwrap();
|
||||||
|
return Some(KeyStroke { code: KeyCode::Char(ch), modifiers: KeyModifiers::empty() });
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn string_to_keycode(s: &str) -> Option<KeyCode> {
|
||||||
|
Some(match s {
|
||||||
|
"left" => KeyCode::Left,
|
||||||
|
"right" => KeyCode::Right,
|
||||||
|
"up" => KeyCode::Up,
|
||||||
|
"down" => KeyCode::Down,
|
||||||
|
"esc" => KeyCode::Esc,
|
||||||
|
"enter" => KeyCode::Enter,
|
||||||
|
"backspace" => KeyCode::Backspace,
|
||||||
|
"delete" => KeyCode::Delete,
|
||||||
|
"tab" => KeyCode::Tab,
|
||||||
|
"home" => KeyCode::Home,
|
||||||
|
"end" => KeyCode::End,
|
||||||
|
"$" => KeyCode::Char('$'),
|
||||||
|
"0" => KeyCode::Char('0'),
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -4,22 +4,21 @@ pub mod canvas;
|
|||||||
pub mod editor;
|
pub mod editor;
|
||||||
pub mod data_provider;
|
pub mod data_provider;
|
||||||
|
|
||||||
// Only include suggestions module if feature is enabled
|
|
||||||
#[cfg(feature = "suggestions")]
|
#[cfg(feature = "suggestions")]
|
||||||
pub mod suggestions;
|
pub mod suggestions;
|
||||||
|
|
||||||
// Only include validation module if feature is enabled
|
|
||||||
#[cfg(feature = "validation")]
|
#[cfg(feature = "validation")]
|
||||||
pub mod validation;
|
pub mod validation;
|
||||||
|
|
||||||
// First-class textarea module and exports
|
|
||||||
#[cfg(feature = "textarea")]
|
#[cfg(feature = "textarea")]
|
||||||
pub mod textarea;
|
pub mod textarea;
|
||||||
|
|
||||||
// Only include computed module if feature is enabled
|
|
||||||
#[cfg(feature = "computed")]
|
#[cfg(feature = "computed")]
|
||||||
pub mod computed;
|
pub mod computed;
|
||||||
|
|
||||||
|
#[cfg(feature = "keymap")]
|
||||||
|
pub mod keymap;
|
||||||
|
|
||||||
#[cfg(feature = "cursor-style")]
|
#[cfg(feature = "cursor-style")]
|
||||||
pub use canvas::CursorManager;
|
pub use canvas::CursorManager;
|
||||||
|
|
||||||
@@ -71,6 +70,8 @@ pub use canvas::gui::{CanvasDisplayOptions, OverflowMode};
|
|||||||
#[cfg(all(feature = "gui", feature = "suggestions"))]
|
#[cfg(all(feature = "gui", feature = "suggestions"))]
|
||||||
pub use suggestions::gui::render_suggestions_dropdown;
|
pub use suggestions::gui::render_suggestions_dropdown;
|
||||||
|
|
||||||
|
#[cfg(feature = "keymap")]
|
||||||
|
pub use keymap::{CanvasKeyMap, KeyEventOutcome};
|
||||||
|
|
||||||
#[cfg(feature = "textarea")]
|
#[cfg(feature = "textarea")]
|
||||||
pub use textarea::{TextArea, TextAreaProvider, TextAreaState, TextAreaEditor};
|
pub use textarea::{TextArea, TextAreaProvider, TextAreaState, TextAreaEditor};
|
||||||
|
|||||||
Reference in New Issue
Block a user