Compare commits

...

8 Commits

Author SHA1 Message Date
Priec
8788323c62 fixed canvas library 2025-07-31 20:44:23 +02:00
Priec
5b64996462 example with debug stuff 2025-07-31 19:05:57 +02:00
Priec
3f4380ff48 documented code now 2025-07-31 17:29:03 +02:00
Priec
59a29aa54b not working example to canvas crate, improving and fixing now 2025-07-31 15:07:28 +02:00
Priec
5d084bf822 fixed working canvas in client, need more fixes now 2025-07-31 14:44:47 +02:00
Priec
ebe4adaa5d bug is present, i cant type or move in canvas from client 2025-07-31 13:39:38 +02:00
Priec
c3441647e0 docs and config adjustement 2025-07-31 13:18:27 +02:00
Priec
574803988d introspection to generated config now works 2025-07-31 12:31:21 +02:00
19 changed files with 1829 additions and 786 deletions

View File

@@ -29,3 +29,7 @@ tokio-test = "0.4.4"
[features]
default = []
gui = ["ratatui"]
[[example]]
name = "ratatui_demo"
path = "examples/ratatui_demo.rs"

View File

@@ -0,0 +1,77 @@
git status
On branch main
Your branch is ahead of 'origin/main' by 1 commit.
(use "git push" to publish your local commits)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: src/canvas/actions/handlers/edit.rs
modified: src/canvas/actions/types.rs
no changes added to commit (use "git add" and/or "git commit -a")
git --no-pager diff
diff --git a/canvas/src/canvas/actions/handlers/edit.rs b/canvas/src/canvas/actions/handlers/edit.rs
index a26fe6f..fa1becb 100644
--- a/canvas/src/canvas/actions/handlers/edit.rs
+++ b/canvas/src/canvas/actions/handlers/edit.rs
@@ -29,6 +29,21 @@ pub async fn handle_edit_action<S: CanvasState>(
Ok(ActionResult::success())
}
+ CanvasAction::SelectAll => {
+ // Select all text in current field
+ let current_input = state.get_current_input();
+ let text_length = current_input.len();
+
+ // Set cursor to start and select all
+ state.set_current_cursor_pos(0);
+ // TODO: You'd need to add selection state to CanvasState trait
+ // For now, just move cursor to end to "select" all
+ state.set_current_cursor_pos(text_length);
+ *ideal_cursor_column = text_length;
+
+ Ok(ActionResult::success_with_message(&format!("Selected all {} characters", text_length)))
+ }
+
CanvasAction::DeleteBackward => {
let cursor_pos = state.current_cursor_pos();
if cursor_pos > 0 {
@@ -323,6 +338,13 @@ impl ActionHandlerIntrospection for EditHandler {
is_required: false,
});
+ actions.push(ActionSpec {
+ name: "select_all".to_string(),
+ description: "Select all text in current field".to_string(),
+ examples: vec!["Ctrl+a".to_string()],
+ is_required: false, // Optional action
+ });
+
HandlerCapabilities {
mode_name: "edit".to_string(),
actions,
diff --git a/canvas/src/canvas/actions/types.rs b/canvas/src/canvas/actions/types.rs
index 433a4d5..3794596 100644
--- a/canvas/src/canvas/actions/types.rs
+++ b/canvas/src/canvas/actions/types.rs
@@ -31,6 +31,8 @@ pub enum CanvasAction {
NextField,
PrevField,
+ SelectAll,
+
// Autocomplete actions
TriggerAutocomplete,
SuggestionUp,
@@ -62,6 +64,7 @@ impl CanvasAction {
"move_word_end_prev" => Self::MoveWordEndPrev,
"next_field" => Self::NextField,
"prev_field" => Self::PrevField,
+ "select_all" => Self::SelectAll,
"trigger_autocomplete" => Self::TriggerAutocomplete,
"suggestion_up" => Self::SuggestionUp,
"suggestion_down" => Self::SuggestionDown,
╭─    ~/Doc/p/komp_ac/canvas  on   main ⇡1 !2 
╰─

View File

@@ -0,0 +1,367 @@
use std::io;
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers},
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,
modes::{AppMode, HighlightState, ModeManager},
state::{ActionContext, CanvasState},
theme::CanvasTheme,
},
config::CanvasConfig,
dispatcher::ActionDispatcher,
CanvasAction,
};
// Simple theme implementation
#[derive(Clone)]
struct DemoTheme;
impl CanvasTheme for DemoTheme {
fn bg(&self) -> Color { Color::Reset }
fn fg(&self) -> Color { Color::White }
fn accent(&self) -> Color { Color::Cyan }
fn secondary(&self) -> Color { Color::Gray }
fn highlight(&self) -> Color { Color::Yellow }
fn highlight_bg(&self) -> Color { Color::DarkGray }
fn warning(&self) -> Color { Color::Red }
fn border(&self) -> Color { Color::Gray }
}
// Demo form state
struct DemoFormState {
fields: Vec<String>,
field_names: Vec<String>,
current_field: usize,
cursor_pos: usize,
mode: AppMode,
highlight_state: HighlightState,
has_changes: bool,
ideal_cursor_column: usize,
last_action: Option<String>,
debug_message: String,
}
impl DemoFormState {
fn new() -> Self {
Self {
fields: vec![
"John Doe".to_string(),
"john.doe@example.com".to_string(),
"+1 234 567 8900".to_string(),
"123 Main Street Apt 4B".to_string(),
"San Francisco".to_string(),
"This is a test comment with multiple words".to_string(),
],
field_names: vec![
"Name".to_string(),
"Email".to_string(),
"Phone".to_string(),
"Address".to_string(),
"City".to_string(),
"Comments".to_string(),
],
current_field: 0,
cursor_pos: 0,
mode: AppMode::ReadOnly,
highlight_state: HighlightState::Off,
has_changes: false,
ideal_cursor_column: 0,
last_action: None,
debug_message: "Ready".to_string(),
}
}
fn enter_edit_mode(&mut self) {
if ModeManager::can_enter_edit_mode(self.mode) {
self.mode = AppMode::Edit;
self.debug_message = "Entered EDIT mode".to_string();
}
}
fn enter_readonly_mode(&mut self) {
if ModeManager::can_enter_read_only_mode(self.mode) {
self.mode = AppMode::ReadOnly;
self.highlight_state = HighlightState::Off;
self.debug_message = "Entered READ-ONLY mode".to_string();
}
}
fn enter_highlight_mode(&mut self) {
if ModeManager::can_enter_highlight_mode(self.mode) {
self.mode = AppMode::Highlight;
self.highlight_state = HighlightState::Characterwise {
anchor: (self.current_field, self.cursor_pos),
};
self.debug_message = "Entered VISUAL mode".to_string();
}
}
}
impl CanvasState for DemoFormState {
fn current_field(&self) -> usize {
self.current_field
}
fn current_cursor_pos(&self) -> usize {
self.cursor_pos
}
fn set_current_field(&mut self, index: usize) {
self.current_field = index.min(self.fields.len().saturating_sub(1));
self.cursor_pos = self.fields[self.current_field].len();
}
fn set_current_cursor_pos(&mut self, pos: usize) {
let max_pos = self.fields[self.current_field].len();
self.cursor_pos = pos.min(max_pos);
}
fn current_mode(&self) -> AppMode {
self.mode
}
fn get_current_input(&self) -> &str {
&self.fields[self.current_field]
}
fn get_current_input_mut(&mut self) -> &mut String {
&mut self.fields[self.current_field]
}
fn inputs(&self) -> Vec<&String> {
self.fields.iter().collect()
}
fn fields(&self) -> Vec<&str> {
self.field_names.iter().map(|s| s.as_str()).collect()
}
fn has_unsaved_changes(&self) -> bool {
self.has_changes
}
fn set_has_unsaved_changes(&mut self, changed: bool) {
self.has_changes = changed;
}
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
match action {
CanvasAction::Custom(cmd) => {
match cmd.as_str() {
"enter_edit_mode" => {
self.enter_edit_mode();
Some("Entered edit mode".to_string())
}
"enter_readonly_mode" => {
self.enter_readonly_mode();
Some("Entered read-only mode".to_string())
}
"enter_highlight_mode" => {
self.enter_highlight_mode();
Some("Entered highlight mode".to_string())
}
_ => None,
}
}
_ => None,
}
}
}
async fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut state: DemoFormState, config: CanvasConfig) -> io::Result<()> {
let theme = DemoTheme;
loop {
terminal.draw(|f| ui(f, &state, &theme))?;
if let Event::Key(key) = event::read()? {
// Handle quit
if (key.code == KeyCode::Char('q') && key.modifiers.contains(KeyModifiers::CONTROL)) ||
(key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL)) ||
key.code == KeyCode::F(10) {
break;
}
let is_edit_mode = state.mode == AppMode::Edit;
let mut handled = false;
// First priority: Try to dispatch through config system
let mut ideal_cursor = state.ideal_cursor_column;
if let Ok(Some(result)) = ActionDispatcher::dispatch_key(
key.code,
key.modifiers,
&mut state,
&mut ideal_cursor,
is_edit_mode,
false,
).await {
state.ideal_cursor_column = ideal_cursor;
state.debug_message = format!("Config handled: {:?}", key.code);
// Mark as changed for text modification keys in edit mode
if is_edit_mode {
match key.code {
KeyCode::Char(_) | KeyCode::Backspace | KeyCode::Delete => {
state.set_has_unsaved_changes(true);
}
_ => {}
}
}
handled = true;
}
// Second priority: Handle character input in edit mode
if !handled && is_edit_mode {
if let KeyCode::Char(c) = key.code {
if !key.modifiers.contains(KeyModifiers::CONTROL) && !key.modifiers.contains(KeyModifiers::ALT) {
let action = CanvasAction::InsertChar(c);
let mut ideal_cursor = state.ideal_cursor_column;
if let Ok(_) = ActionDispatcher::dispatch_with_config(
action,
&mut state,
&mut ideal_cursor,
Some(&config),
).await {
state.ideal_cursor_column = ideal_cursor;
state.set_has_unsaved_changes(true);
state.debug_message = format!("Inserted char: '{}'", c);
handled = true;
}
}
}
}
// Third priority: Fallback mode transitions
if !handled {
match (state.mode, key.code) {
(AppMode::ReadOnly, KeyCode::Char('i') | KeyCode::Char('a') | KeyCode::Insert) => {
state.enter_edit_mode();
if key.code == KeyCode::Char('a') {
state.cursor_pos = state.fields[state.current_field].len();
}
state.debug_message = format!("Entered edit mode via {:?}", key.code);
handled = true;
}
(AppMode::ReadOnly, KeyCode::Char('v')) => {
state.enter_highlight_mode();
state.debug_message = "Entered visual mode".to_string();
handled = true;
}
(_, KeyCode::Esc) => {
state.enter_readonly_mode();
state.debug_message = "Entered read-only mode".to_string();
handled = true;
}
_ => {}
}
}
if !handled {
state.debug_message = format!("Unhandled key: {:?}", key.code);
}
}
}
Ok(())
}
fn ui(f: &mut Frame, state: &DemoFormState, theme: &DemoTheme) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(8),
Constraint::Length(4),
])
.split(f.area());
// Render the canvas form
render_canvas(
f,
chunks[0],
state,
theme,
state.mode == AppMode::Edit,
&state.highlight_state,
);
// Render status bar
let mode_text = match state.mode {
AppMode::Edit => "EDIT",
AppMode::ReadOnly => "NORMAL",
AppMode::Highlight => "VISUAL",
AppMode::General => "GENERAL",
AppMode::Command => "COMMAND",
};
let status_text = if state.has_changes {
format!("-- {} -- [Modified]", mode_text)
} else {
format!("-- {} --", mode_text)
};
let position_text = format!("Field: {}/{} | Cursor: {} | Column: {}",
state.current_field + 1,
state.fields.len(),
state.cursor_pos,
state.ideal_cursor_column);
let help_text = match state.mode {
AppMode::ReadOnly => "hjkl/arrows: Move | Tab/Shift+Tab: Fields | w/b/e: Words | 0/$: Line | gg/G: File | i/a: Edit | v: Visual | F10: Quit",
AppMode::Edit => "Type to edit | hjkl/arrows: Move | Tab/Enter: Next field | Backspace/Delete: Delete | Home/End: Line | Esc: Normal | F10: Quit",
AppMode::Highlight => "hjkl/arrows: Select | w/b/e: Words | 0/$: Line | Esc: Normal | F10: Quit",
_ => "Esc: Normal | F10: Quit",
};
let status = Paragraph::new(vec![
Line::from(Span::styled(status_text, Style::default().fg(theme.accent()))),
Line::from(Span::styled(position_text, Style::default().fg(theme.fg()))),
Line::from(Span::styled(state.debug_message.clone(), Style::default().fg(theme.warning()))),
Line::from(Span::styled(help_text, Style::default().fg(theme.secondary()))),
])
.block(Block::default().borders(Borders::ALL).title("Status"));
f.render_widget(status, chunks[1]);
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = CanvasConfig::load();
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let state = DemoFormState::new();
let res = run_app(&mut terminal, state, config).await;
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{:?}", err);
}
Ok(())
}

View File

@@ -31,12 +31,9 @@ pub async fn execute_canvas_action_with_autocomplete<S: CanvasState + Autocomple
// 3. AUTO-TRIGGER LOGIC: Check if we should activate/deactivate autocomplete
if let Some(cfg) = config {
println!("{:?}, {}", action, cfg.should_auto_trigger_autocomplete());
if cfg.should_auto_trigger_autocomplete() {
println!("AUTO-TRIGGER");
match action {
CanvasAction::InsertChar(_) => {
println!("AUTO-T on Ins");
let current_field = state.current_field();
let current_input = state.get_current_input();
@@ -44,13 +41,11 @@ pub async fn execute_canvas_action_with_autocomplete<S: CanvasState + Autocomple
&& !state.is_autocomplete_active()
&& current_input.len() >= 1
{
println!("ACT AUTOC");
state.activate_autocomplete();
}
}
CanvasAction::NextField | CanvasAction::PrevField => {
println!("AUTO-T on nav");
let current_field = state.current_field();
if state.supports_autocomplete(current_field) && !state.is_autocomplete_active() {

View File

@@ -1,14 +1,32 @@
// src/canvas/actions/handlers/edit.rs
//! Edit mode action handler
//!
//! Handles user input when in edit mode, supporting text entry, deletion,
//! and cursor movement with edit-specific behavior (cursor can go past end of text).
use crate::canvas::actions::types::{CanvasAction, ActionResult};
use crate::config::introspection::{ActionHandlerIntrospection, HandlerCapabilities, ActionSpec};
use crate::canvas::actions::movement::*;
use crate::canvas::state::CanvasState;
use crate::config::CanvasConfig;
use anyhow::Result;
const FOR_EDIT_MODE: bool = true; // Edit mode flag
/// Edit mode uses cursor-past-end behavior for text insertion
const FOR_EDIT_MODE: bool = true;
/// Empty struct that implements edit mode capabilities
pub struct EditHandler;
/// Handle actions in edit mode with edit-specific cursor behavior
///
/// Edit mode allows text modification and uses cursor positioning that can
/// go past the end of existing text to facilitate insertion.
///
/// # Arguments
/// * `action` - The action to perform
/// * `state` - Mutable canvas state
/// * `ideal_cursor_column` - Desired column for vertical movement (maintained across line changes)
/// * `config` - Optional configuration for behavior customization
pub async fn handle_edit_action<S: CanvasState>(
action: CanvasAction,
state: &mut S,
@@ -17,6 +35,7 @@ pub async fn handle_edit_action<S: CanvasState>(
) -> Result<ActionResult> {
match action {
CanvasAction::InsertChar(c) => {
// Insert character at cursor position and advance cursor
let cursor_pos = state.current_cursor_pos();
let input = state.get_current_input_mut();
input.insert(cursor_pos, c);
@@ -27,6 +46,7 @@ pub async fn handle_edit_action<S: CanvasState>(
}
CanvasAction::DeleteBackward => {
// Delete character before cursor (Backspace behavior)
let cursor_pos = state.current_cursor_pos();
if cursor_pos > 0 {
let input = state.get_current_input_mut();
@@ -39,6 +59,7 @@ pub async fn handle_edit_action<S: CanvasState>(
}
CanvasAction::DeleteForward => {
// Delete character at cursor position (Delete key behavior)
let cursor_pos = state.current_cursor_pos();
let input = state.get_current_input_mut();
if cursor_pos < input.len() {
@@ -48,6 +69,7 @@ pub async fn handle_edit_action<S: CanvasState>(
Ok(ActionResult::success())
}
// Cursor movement actions
CanvasAction::MoveLeft => {
let new_pos = move_left(state.current_cursor_pos());
state.set_current_cursor_pos(new_pos);
@@ -64,8 +86,8 @@ pub async fn handle_edit_action<S: CanvasState>(
Ok(ActionResult::success())
}
// Field navigation (treating single-line fields as "lines")
CanvasAction::MoveUp => {
// For single-line fields, move to previous field
let current_field = state.current_field();
if current_field > 0 {
state.set_current_field(current_field - 1);
@@ -77,7 +99,6 @@ pub async fn handle_edit_action<S: CanvasState>(
}
CanvasAction::MoveDown => {
// For single-line fields, move to next field
let current_field = state.current_field();
let total_fields = state.fields().len();
if current_field < total_fields - 1 {
@@ -89,6 +110,7 @@ pub async fn handle_edit_action<S: CanvasState>(
Ok(ActionResult::success())
}
// Line-based movement
CanvasAction::MoveLineStart => {
let new_pos = line_start_position();
state.set_current_cursor_pos(new_pos);
@@ -104,6 +126,7 @@ pub async fn handle_edit_action<S: CanvasState>(
Ok(ActionResult::success())
}
// Document-level movement (first/last field)
CanvasAction::MoveFirstLine => {
state.set_current_field(0);
let current_input = state.get_current_input();
@@ -123,6 +146,7 @@ pub async fn handle_edit_action<S: CanvasState>(
Ok(ActionResult::success())
}
// Word-based movement
CanvasAction::MoveWordNext => {
let current_input = state.get_current_input();
if !current_input.is_empty() {
@@ -163,6 +187,7 @@ pub async fn handle_edit_action<S: CanvasState>(
Ok(ActionResult::success())
}
// Field navigation with wrapping behavior
CanvasAction::NextField | CanvasAction::PrevField => {
let current_field = state.current_field();
let total_fields = state.fields().len();
@@ -170,16 +195,16 @@ pub async fn handle_edit_action<S: CanvasState>(
let new_field = match action {
CanvasAction::NextField => {
if config.map_or(true, |c| c.behavior.wrap_around_fields) {
(current_field + 1) % total_fields
(current_field + 1) % total_fields // Wrap to first field
} else {
(current_field + 1).min(total_fields - 1)
(current_field + 1).min(total_fields - 1) // Stop at last field
}
}
CanvasAction::PrevField => {
if config.map_or(true, |c| c.behavior.wrap_around_fields) {
if current_field == 0 { total_fields - 1 } else { current_field - 1 }
if current_field == 0 { total_fields - 1 } else { current_field - 1 } // Wrap to last field
} else {
current_field.saturating_sub(1)
current_field.saturating_sub(1) // Stop at first field
}
}
_ => unreachable!(),
@@ -201,3 +226,151 @@ pub async fn handle_edit_action<S: CanvasState>(
}
}
}
impl ActionHandlerIntrospection for EditHandler {
/// Report all actions this handler supports with examples and requirements
/// Used for automatic config generation and validation
fn introspect() -> HandlerCapabilities {
let mut actions = Vec::new();
// REQUIRED ACTIONS - These must be configured for edit mode to work properly
actions.push(ActionSpec {
name: "move_left".to_string(),
description: "Move cursor one position to the left".to_string(),
examples: vec!["Left".to_string(), "h".to_string()],
is_required: true,
});
actions.push(ActionSpec {
name: "move_right".to_string(),
description: "Move cursor one position to the right".to_string(),
examples: vec!["Right".to_string(), "l".to_string()],
is_required: true,
});
actions.push(ActionSpec {
name: "move_up".to_string(),
description: "Move to previous field or line".to_string(),
examples: vec!["Up".to_string(), "k".to_string()],
is_required: true,
});
actions.push(ActionSpec {
name: "move_down".to_string(),
description: "Move to next field or line".to_string(),
examples: vec!["Down".to_string(), "j".to_string()],
is_required: true,
});
actions.push(ActionSpec {
name: "delete_char_backward".to_string(),
description: "Delete character before cursor (Backspace)".to_string(),
examples: vec!["Backspace".to_string()],
is_required: true,
});
actions.push(ActionSpec {
name: "next_field".to_string(),
description: "Move to next input field".to_string(),
examples: vec!["Tab".to_string(), "Enter".to_string()],
is_required: true,
});
actions.push(ActionSpec {
name: "prev_field".to_string(),
description: "Move to previous input field".to_string(),
examples: vec!["Shift+Tab".to_string()],
is_required: true,
});
// OPTIONAL ACTIONS - These enhance functionality but aren't required
actions.push(ActionSpec {
name: "move_word_next".to_string(),
description: "Move cursor to start of next word".to_string(),
examples: vec!["Ctrl+Right".to_string(), "w".to_string()],
is_required: false,
});
actions.push(ActionSpec {
name: "move_word_prev".to_string(),
description: "Move cursor to start of previous word".to_string(),
examples: vec!["Ctrl+Left".to_string(), "b".to_string()],
is_required: false,
});
actions.push(ActionSpec {
name: "move_word_end".to_string(),
description: "Move cursor to end of current/next word".to_string(),
examples: vec!["e".to_string()],
is_required: false,
});
actions.push(ActionSpec {
name: "move_word_end_prev".to_string(),
description: "Move cursor to end of previous word".to_string(),
examples: vec!["ge".to_string()],
is_required: false,
});
actions.push(ActionSpec {
name: "move_line_start".to_string(),
description: "Move cursor to beginning of line".to_string(),
examples: vec!["Home".to_string(), "0".to_string()],
is_required: false,
});
actions.push(ActionSpec {
name: "move_line_end".to_string(),
description: "Move cursor to end of line".to_string(),
examples: vec!["End".to_string(), "$".to_string()],
is_required: false,
});
actions.push(ActionSpec {
name: "move_first_line".to_string(),
description: "Move to first field".to_string(),
examples: vec!["Ctrl+Home".to_string(), "gg".to_string()],
is_required: false,
});
actions.push(ActionSpec {
name: "move_last_line".to_string(),
description: "Move to last field".to_string(),
examples: vec!["Ctrl+End".to_string(), "G".to_string()],
is_required: false,
});
actions.push(ActionSpec {
name: "delete_char_forward".to_string(),
description: "Delete character after cursor (Delete key)".to_string(),
examples: vec!["Delete".to_string()],
is_required: false,
});
HandlerCapabilities {
mode_name: "edit".to_string(),
actions,
auto_handled: vec![
"insert_char".to_string(), // Any printable character is inserted automatically
],
}
}
fn validate_capabilities() -> Result<(), String> {
// TODO: Could add runtime validation that the handler actually
// implements all the actions it claims to support
// For now, just validate that we have the essential actions
let caps = Self::introspect();
let required_count = caps.actions.iter().filter(|a| a.is_required).count();
if required_count < 7 { // We expect at least 7 required actions
return Err(format!(
"Edit handler claims only {} required actions, expected at least 7",
required_count
));
}
Ok(())
}
}

View File

@@ -1,12 +1,16 @@
// src/canvas/actions/handlers/highlight.rs
use crate::canvas::actions::types::{CanvasAction, ActionResult};
use crate::config::introspection::{ActionHandlerIntrospection, HandlerCapabilities, ActionSpec};
use crate::canvas::actions::movement::*;
use crate::canvas::state::CanvasState;
use crate::config::CanvasConfig;
use anyhow::Result;
const FOR_EDIT_MODE: bool = false; // Highlight mode uses read-only cursor behavior
pub struct HighlightHandler;
/// Handle actions in highlight/visual mode
/// TODO: Implement selection logic and highlight-specific behaviors
@@ -104,3 +108,98 @@ pub async fn handle_highlight_action<S: CanvasState>(
}
}
}
impl ActionHandlerIntrospection for HighlightHandler {
fn introspect() -> HandlerCapabilities {
let mut actions = Vec::new();
// For now, highlight mode uses similar movement to readonly
// but this will be discovered from actual implementation
// REQUIRED ACTIONS - Basic movement in highlight mode
actions.push(ActionSpec {
name: "move_left".to_string(),
description: "Move cursor left and extend selection".to_string(),
examples: vec!["h".to_string(), "Left".to_string()],
is_required: true,
});
actions.push(ActionSpec {
name: "move_right".to_string(),
description: "Move cursor right and extend selection".to_string(),
examples: vec!["l".to_string(), "Right".to_string()],
is_required: true,
});
actions.push(ActionSpec {
name: "move_up".to_string(),
description: "Move up and extend selection".to_string(),
examples: vec!["k".to_string(), "Up".to_string()],
is_required: true,
});
actions.push(ActionSpec {
name: "move_down".to_string(),
description: "Move down and extend selection".to_string(),
examples: vec!["j".to_string(), "Down".to_string()],
is_required: true,
});
// OPTIONAL ACTIONS - Advanced highlight movement
actions.push(ActionSpec {
name: "move_word_next".to_string(),
description: "Move to next word and extend selection".to_string(),
examples: vec!["w".to_string()],
is_required: false,
});
actions.push(ActionSpec {
name: "move_word_end".to_string(),
description: "Move to word end and extend selection".to_string(),
examples: vec!["e".to_string()],
is_required: false,
});
actions.push(ActionSpec {
name: "move_word_prev".to_string(),
description: "Move to previous word and extend selection".to_string(),
examples: vec!["b".to_string()],
is_required: false,
});
actions.push(ActionSpec {
name: "move_line_start".to_string(),
description: "Move to line start and extend selection".to_string(),
examples: vec!["0".to_string()],
is_required: false,
});
actions.push(ActionSpec {
name: "move_line_end".to_string(),
description: "Move to line end and extend selection".to_string(),
examples: vec!["$".to_string()],
is_required: false,
});
HandlerCapabilities {
mode_name: "highlight".to_string(),
actions,
auto_handled: vec![], // Highlight mode has no auto-handled actions
}
}
fn validate_capabilities() -> Result<(), String> {
let caps = Self::introspect();
let required_count = caps.actions.iter().filter(|a| a.is_required).count();
if required_count < 4 { // We expect at least 4 required actions (basic movement)
return Err(format!(
"Highlight handler claims only {} required actions, expected at least 4",
required_count
));
}
Ok(())
}
}

View File

@@ -1,6 +1,7 @@
// src/canvas/actions/handlers/readonly.rs
use crate::canvas::actions::types::{CanvasAction, ActionResult};
use crate::config::introspection::{ActionHandlerIntrospection, HandlerCapabilities, ActionSpec};
use crate::canvas::actions::movement::*;
use crate::canvas::state::CanvasState;
use crate::config::CanvasConfig;
@@ -191,3 +192,131 @@ pub async fn handle_readonly_action<S: CanvasState>(
}
}
}
pub struct ReadOnlyHandler;
impl ActionHandlerIntrospection for ReadOnlyHandler {
fn introspect() -> HandlerCapabilities {
let mut actions = Vec::new();
// REQUIRED ACTIONS - Navigation is essential in read-only mode
actions.push(ActionSpec {
name: "move_left".to_string(),
description: "Move cursor one position to the left".to_string(),
examples: vec!["h".to_string(), "Left".to_string()],
is_required: true,
});
actions.push(ActionSpec {
name: "move_right".to_string(),
description: "Move cursor one position to the right".to_string(),
examples: vec!["l".to_string(), "Right".to_string()],
is_required: true,
});
actions.push(ActionSpec {
name: "move_up".to_string(),
description: "Move to previous field".to_string(),
examples: vec!["k".to_string(), "Up".to_string()],
is_required: true,
});
actions.push(ActionSpec {
name: "move_down".to_string(),
description: "Move to next field".to_string(),
examples: vec!["j".to_string(), "Down".to_string()],
is_required: true,
});
// OPTIONAL ACTIONS - Advanced navigation features
actions.push(ActionSpec {
name: "move_word_next".to_string(),
description: "Move cursor to start of next word".to_string(),
examples: vec!["w".to_string()],
is_required: false,
});
actions.push(ActionSpec {
name: "move_word_prev".to_string(),
description: "Move cursor to start of previous word".to_string(),
examples: vec!["b".to_string()],
is_required: false,
});
actions.push(ActionSpec {
name: "move_word_end".to_string(),
description: "Move cursor to end of current/next word".to_string(),
examples: vec!["e".to_string()],
is_required: false,
});
actions.push(ActionSpec {
name: "move_word_end_prev".to_string(),
description: "Move cursor to end of previous word".to_string(),
examples: vec!["ge".to_string()],
is_required: false,
});
actions.push(ActionSpec {
name: "move_line_start".to_string(),
description: "Move cursor to beginning of line".to_string(),
examples: vec!["0".to_string()],
is_required: false,
});
actions.push(ActionSpec {
name: "move_line_end".to_string(),
description: "Move cursor to end of line".to_string(),
examples: vec!["$".to_string()],
is_required: false,
});
actions.push(ActionSpec {
name: "move_first_line".to_string(),
description: "Move to first field".to_string(),
examples: vec!["gg".to_string()],
is_required: false,
});
actions.push(ActionSpec {
name: "move_last_line".to_string(),
description: "Move to last field".to_string(),
examples: vec!["G".to_string()],
is_required: false,
});
actions.push(ActionSpec {
name: "next_field".to_string(),
description: "Move to next input field".to_string(),
examples: vec!["Tab".to_string()],
is_required: false,
});
actions.push(ActionSpec {
name: "prev_field".to_string(),
description: "Move to previous input field".to_string(),
examples: vec!["Shift+Tab".to_string()],
is_required: false,
});
HandlerCapabilities {
mode_name: "read_only".to_string(),
actions,
auto_handled: vec![], // Read-only mode has no auto-handled actions
}
}
fn validate_capabilities() -> Result<(), String> {
let caps = Self::introspect();
let required_count = caps.actions.iter().filter(|a| a.is_required).count();
if required_count < 4 { // We expect at least 4 required actions (basic movement)
return Err(format!(
"ReadOnly handler claims only {} required actions, expected at least 4",
required_count
));
}
Ok(())
}
}

View File

@@ -1,48 +1,107 @@
// src/canvas/state.rs
//! Canvas state trait and related types
//!
//! This module defines the core trait that any form or input system must implement
//! to work with the canvas library.
use crate::canvas::actions::CanvasAction;
use crate::canvas::modes::AppMode;
/// Context passed to feature-specific action handlers
/// Context information passed to feature-specific action handlers
#[derive(Debug)]
pub struct ActionContext {
pub key_code: Option<crossterm::event::KeyCode>, // Kept for backwards compatibility
/// Original key code that triggered this action (for backwards compatibility)
pub key_code: Option<crossterm::event::KeyCode>,
/// Current ideal cursor column for vertical movement
pub ideal_cursor_column: usize,
/// Current input text
pub current_input: String,
/// Current field index
pub current_field: usize,
}
/// Core trait that any form-like state must implement to work with the canvas system.
/// This enables the same mode behaviors (edit, read-only, highlight) to work across
/// any implementation - login forms, data entry forms, configuration screens, etc.
/// Core trait that any form-like state must implement to work with canvas
///
/// This trait enables the same mode behaviors (edit, read-only, highlight) to work
/// across any implementation - login forms, data entry forms, configuration screens, etc.
///
/// # Required Implementation
///
/// Your struct needs to track:
/// - Current field index and cursor position
/// - All input field values
/// - Current interaction mode
/// - Whether there are unsaved changes
///
/// # Example Implementation
///
/// ```rust
/// struct MyForm {
/// fields: Vec<String>,
/// current_field: usize,
/// cursor_pos: usize,
/// mode: AppMode,
/// dirty: bool,
/// }
///
/// impl CanvasState for MyForm {
/// fn current_field(&self) -> usize { self.current_field }
/// fn current_cursor_pos(&self) -> usize { self.cursor_pos }
/// // ... implement other required methods
/// }
/// ```
pub trait CanvasState {
// --- Core Navigation ---
/// Get current field index (0-based)
fn current_field(&self) -> usize;
/// Get current cursor position within the current field
fn current_cursor_pos(&self) -> usize;
/// Set current field index (should clamp to valid range)
fn set_current_field(&mut self, index: usize);
/// Set cursor position within current field (should clamp to valid range)
fn set_current_cursor_pos(&mut self, pos: usize);
// --- Mode Information ---
/// Get current interaction mode (edit, read-only, highlight, etc.)
fn current_mode(&self) -> AppMode;
// --- Data Access ---
/// Get immutable reference to current field's text
fn get_current_input(&self) -> &str;
/// Get mutable reference to current field's text
fn get_current_input_mut(&mut self) -> &mut String;
/// Get all input values as immutable references
fn inputs(&self) -> Vec<&String>;
/// Get all field names/labels
fn fields(&self) -> Vec<&str>;
// --- State Management ---
/// Check if there are unsaved changes
fn has_unsaved_changes(&self) -> bool;
/// Mark whether there are unsaved changes
fn set_has_unsaved_changes(&mut self, changed: bool);
// --- Feature-specific action handling ---
// --- Optional Overrides ---
/// Feature-specific action handling (Type-safe)
/// Handle application-specific actions not covered by standard handlers
/// Return Some(message) if the action was handled, None to use standard handling
fn handle_feature_action(&mut self, _action: &CanvasAction, _context: &ActionContext) -> Option<String> {
None // Default: no feature-specific handling
None // Default: no custom handling
}
// --- Display Overrides (for links, computed values, etc.) ---
/// Get display value for a field (may differ from actual value)
/// Used for things like password masking or computed display values
fn get_display_value_for_field(&self, index: usize) -> &str {
self.inputs()
.get(index)
@@ -50,6 +109,8 @@ pub trait CanvasState {
.unwrap_or("")
}
/// Check if a field has a custom display value
/// Return true if get_display_value_for_field returns something different than the actual value
fn has_display_override(&self, _index: usize) -> bool {
false
}

View File

@@ -1,86 +1,73 @@
// canvas/src/config.rs
// src/config/config.rs
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crossterm::event::{KeyCode, KeyModifiers};
use anyhow::{Context, Result};
use super::registry::{ActionRegistry, ActionSpec, ModeRegistry};
// Import from sibling modules
use super::registry::ActionRegistry;
use super::validation::{ConfigValidator, ValidationError, ValidationResult, ValidationWarning};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CanvasConfig {
#[serde(default)]
pub keybindings: CanvasKeybindings,
#[serde(default)]
pub behavior: CanvasBehavior,
#[serde(default)]
pub appearance: CanvasAppearance,
pub struct CanvasKeybindings {
pub edit: HashMap<String, Vec<String>>,
pub read_only: HashMap<String, Vec<String>>,
pub global: HashMap<String, Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CanvasKeybindings {
#[serde(default)]
pub read_only: HashMap<String, Vec<String>>,
#[serde(default)]
pub edit: HashMap<String, Vec<String>>,
#[serde(default)]
pub suggestions: HashMap<String, Vec<String>>,
#[serde(default)]
pub global: HashMap<String, Vec<String>>,
impl Default for CanvasKeybindings {
fn default() -> Self {
Self {
edit: HashMap::new(),
read_only: HashMap::new(),
global: HashMap::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CanvasBehavior {
#[serde(default = "default_wrap_around")]
pub confirm_on_save: bool,
pub auto_indent: bool,
pub wrap_search: bool,
pub wrap_around_fields: bool,
#[serde(default = "default_auto_save")]
pub auto_save_on_field_change: bool,
#[serde(default = "default_word_chars")]
pub word_chars: String,
#[serde(default = "default_suggestion_limit")]
pub max_suggestions: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CanvasAppearance {
#[serde(default = "default_cursor_style")]
pub cursor_style: String, // "block", "bar", "underline"
#[serde(default = "default_show_field_numbers")]
pub show_field_numbers: bool,
#[serde(default = "default_highlight_current_field")]
pub highlight_current_field: bool,
}
// Default values
fn default_wrap_around() -> bool { true }
fn default_auto_save() -> bool { false }
fn default_word_chars() -> String { "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_".to_string() }
fn default_suggestion_limit() -> usize { 10 }
fn default_cursor_style() -> String { "block".to_string() }
fn default_show_field_numbers() -> bool { false }
fn default_highlight_current_field() -> bool { true }
impl Default for CanvasBehavior {
fn default() -> Self {
Self {
wrap_around_fields: default_wrap_around(),
auto_save_on_field_change: default_auto_save(),
word_chars: default_word_chars(),
max_suggestions: default_suggestion_limit(),
confirm_on_save: true,
auto_indent: true,
wrap_search: true,
wrap_around_fields: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CanvasAppearance {
pub line_numbers: bool,
pub syntax_highlighting: bool,
pub current_line_highlight: bool,
}
impl Default for CanvasAppearance {
fn default() -> Self {
Self {
cursor_style: default_cursor_style(),
show_field_numbers: default_show_field_numbers(),
highlight_current_field: default_highlight_current_field(),
line_numbers: true,
syntax_highlighting: true,
current_line_highlight: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CanvasConfig {
pub keybindings: CanvasKeybindings,
pub behavior: CanvasBehavior,
pub appearance: CanvasAppearance,
}
impl Default for CanvasConfig {
fn default() -> Self {
Self {
@@ -92,179 +79,335 @@ impl Default for CanvasConfig {
}
impl CanvasKeybindings {
/// Generate complete vim defaults from introspection system
/// This ensures defaults are always in sync with actual handler capabilities
pub fn with_vim_defaults() -> Self {
let registry = ActionRegistry::from_handlers();
Self::generate_from_registry(&registry)
}
/// Generate keybindings from action registry (used by both defaults and config generation)
/// This is the single source of truth for what keybindings should exist
fn generate_from_registry(registry: &ActionRegistry) -> Self {
let mut keybindings = Self::default();
// Read-only mode (vim-style navigation)
// Generate keybindings for each mode discovered by introspection
for (mode_name, mode_registry) in &registry.modes {
let mode_bindings = match mode_name.as_str() {
"edit" => &mut keybindings.edit,
"read_only" => &mut keybindings.read_only,
"highlight" => &mut keybindings.global, // Highlight actions go in global
_ => {
// Handle any future modes discovered by introspection
eprintln!("Warning: Unknown mode '{}' discovered by introspection", mode_name);
continue;
}
};
// Add ALL required actions
for (action_name, action_spec) in &mode_registry.required {
if !action_spec.examples.is_empty() {
mode_bindings.insert(
action_name.clone(),
action_spec.examples.clone()
);
}
}
// Add ALL optional actions
for (action_name, action_spec) in &mode_registry.optional {
if !action_spec.examples.is_empty() {
mode_bindings.insert(
action_name.clone(),
action_spec.examples.clone()
);
}
}
}
keybindings
}
/// Generate a minimal fallback configuration if introspection fails
/// This should rarely be used, but provides safety net
fn minimal_fallback() -> Self {
let mut keybindings = Self::default();
// Absolute minimum required for basic functionality
keybindings.read_only.insert("move_left".to_string(), vec!["h".to_string()]);
keybindings.read_only.insert("move_right".to_string(), vec!["l".to_string()]);
keybindings.read_only.insert("move_up".to_string(), vec!["k".to_string()]);
keybindings.read_only.insert("move_down".to_string(), vec!["j".to_string()]);
keybindings.read_only.insert("move_word_next".to_string(), vec!["w".to_string()]);
keybindings.read_only.insert("move_word_end".to_string(), vec!["e".to_string()]);
keybindings.read_only.insert("move_word_prev".to_string(), vec!["b".to_string()]);
keybindings.read_only.insert("move_word_end_prev".to_string(), vec!["ge".to_string()]);
keybindings.read_only.insert("move_line_start".to_string(), vec!["0".to_string()]);
keybindings.read_only.insert("move_line_end".to_string(), vec!["$".to_string()]);
keybindings.read_only.insert("move_first_line".to_string(), vec!["gg".to_string()]);
keybindings.read_only.insert("move_last_line".to_string(), vec!["G".to_string()]);
keybindings.read_only.insert("next_field".to_string(), vec!["Tab".to_string()]);
keybindings.read_only.insert("prev_field".to_string(), vec!["Shift+Tab".to_string()]);
// Edit mode
keybindings.edit.insert("delete_char_backward".to_string(), vec!["Backspace".to_string()]);
keybindings.edit.insert("delete_char_forward".to_string(), vec!["Delete".to_string()]);
keybindings.edit.insert("move_left".to_string(), vec!["Left".to_string()]);
keybindings.edit.insert("move_right".to_string(), vec!["Right".to_string()]);
keybindings.edit.insert("move_up".to_string(), vec!["Up".to_string()]);
keybindings.edit.insert("move_down".to_string(), vec!["Down".to_string()]);
keybindings.edit.insert("move_line_start".to_string(), vec!["Home".to_string()]);
keybindings.edit.insert("move_line_end".to_string(), vec!["End".to_string()]);
keybindings.edit.insert("move_word_next".to_string(), vec!["Ctrl+Right".to_string()]);
keybindings.edit.insert("move_word_prev".to_string(), vec!["Ctrl+Left".to_string()]);
keybindings.edit.insert("next_field".to_string(), vec!["Tab".to_string()]);
keybindings.edit.insert("prev_field".to_string(), vec!["Shift+Tab".to_string()]);
// Suggestions
keybindings.suggestions.insert("suggestion_up".to_string(), vec!["Up".to_string(), "Ctrl+p".to_string()]);
keybindings.suggestions.insert("suggestion_down".to_string(), vec!["Down".to_string(), "Ctrl+n".to_string()]);
keybindings.suggestions.insert("select_suggestion".to_string(), vec!["Enter".to_string(), "Tab".to_string()]);
keybindings.suggestions.insert("exit_suggestions".to_string(), vec!["Esc".to_string()]);
// Global (works in both modes)
keybindings.global.insert("move_up".to_string(), vec!["Up".to_string()]);
keybindings.global.insert("move_down".to_string(), vec!["Down".to_string()]);
keybindings
}
pub fn with_emacs_defaults() -> Self {
let mut keybindings = Self::default();
/// Validate that generated keybindings match the current introspection state
/// This helps catch when handlers change but defaults become stale
pub fn validate_against_introspection(&self) -> Result<(), Vec<String>> {
let registry = ActionRegistry::from_handlers();
let expected = Self::generate_from_registry(&registry);
let mut errors = Vec::new();
// Emacs-style bindings
keybindings.read_only.insert("move_left".to_string(), vec!["Ctrl+b".to_string()]);
keybindings.read_only.insert("move_right".to_string(), vec!["Ctrl+f".to_string()]);
keybindings.read_only.insert("move_up".to_string(), vec!["Ctrl+p".to_string()]);
keybindings.read_only.insert("move_down".to_string(), vec!["Ctrl+n".to_string()]);
keybindings.read_only.insert("move_word_next".to_string(), vec!["Alt+f".to_string()]);
keybindings.read_only.insert("move_word_prev".to_string(), vec!["Alt+b".to_string()]);
keybindings.read_only.insert("move_line_start".to_string(), vec!["Ctrl+a".to_string()]);
keybindings.read_only.insert("move_line_end".to_string(), vec!["Ctrl+e".to_string()]);
// Check each mode
for (mode_name, expected_bindings) in [
("edit", &expected.edit),
("read_only", &expected.read_only),
("global", &expected.global),
] {
let actual_bindings = match mode_name {
"edit" => &self.edit,
"read_only" => &self.read_only,
"global" => &self.global,
_ => continue,
};
keybindings.edit.insert("delete_char_backward".to_string(), vec!["Ctrl+h".to_string(), "Backspace".to_string()]);
keybindings.edit.insert("delete_char_forward".to_string(), vec!["Ctrl+d".to_string(), "Delete".to_string()]);
// Check for missing actions
for action_name in expected_bindings.keys() {
if !actual_bindings.contains_key(action_name) {
errors.push(format!(
"Missing action '{}' in {} mode (expected by introspection)",
action_name, mode_name
));
}
}
keybindings
// Check for unexpected actions
for action_name in actual_bindings.keys() {
if !expected_bindings.contains_key(action_name) {
errors.push(format!(
"Unexpected action '{}' in {} mode (not found in introspection)",
action_name, mode_name
));
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
impl CanvasConfig {
/// NEW: Load and validate configuration
/// Enhanced load method with introspection validation
pub fn load() -> Self {
match Self::load_and_validate() {
Ok(config) => config,
Err(e) => {
eprintln!("⚠️ Canvas config validation failed: {}", e);
eprintln!(" Using vim defaults. Run CanvasConfig::generate_template() for help.");
eprintln!("Failed to load config file: {}", e);
eprintln!("Using auto-generated defaults from introspection");
Self::default()
}
}
}
/// NEW: Load configuration with validation
/// Load and validate configuration with enhanced introspection checks
pub fn load_and_validate() -> Result<Self> {
// Try to load canvas_config.toml from current directory
let config = if let Ok(config) = Self::from_file(std::path::Path::new("canvas_config.toml")) {
config
} else {
// Fallback to vim defaults
// Use auto-generated defaults if file doesn't exist
eprintln!("Config file not found, using auto-generated defaults");
Self::default()
};
// Validate the configuration
let validator = ConfigValidator::new();
// Validate the configuration against current introspection state
let registry = ActionRegistry::from_handlers();
// Validate handlers are working correctly
if let Err(handler_errors) = registry.validate_against_implementation() {
eprintln!("Handler validation warnings:");
for error in handler_errors {
eprintln!(" - {}", error);
}
}
// Validate the configuration against the dynamic registry
let validator = ConfigValidator::new(registry);
let validation_result = validator.validate_keybindings(&config.keybindings);
if !validation_result.is_valid {
// Print validation errors
eprintln!("Configuration validation failed:");
validator.print_validation_result(&validation_result);
} else if !validation_result.warnings.is_empty() {
eprintln!("Configuration validation warnings:");
validator.print_validation_result(&validation_result);
// Create error with suggestions
let error_msg = format!(
"Configuration validation failed with {} errors",
validation_result.errors.len()
);
return Err(anyhow::anyhow!(error_msg));
}
// Print warnings if any
if !validation_result.warnings.is_empty() {
validator.print_validation_result(&validation_result);
// Optional: Validate that our defaults match introspection
if let Err(sync_errors) = config.keybindings.validate_against_introspection() {
eprintln!("Default keybindings out of sync with introspection:");
for error in sync_errors {
eprintln!(" - {}", error);
}
}
Ok(config)
}
/// NEW: Generate a complete configuration template
pub fn generate_template() -> String {
let registry = ActionRegistry::new();
registry.generate_config_template()
/// Generate a complete configuration template that matches current defaults
/// This ensures the generated config file has the same content as defaults
pub fn generate_complete_template() -> String {
let registry = ActionRegistry::from_handlers();
let defaults = CanvasKeybindings::generate_from_registry(&registry);
let mut template = String::new();
template.push_str("# Canvas Library Configuration\n");
template.push_str("# Auto-generated from handler introspection\n");
template.push_str("# This config contains ALL available actions\n\n");
// Generate sections for each mode
for (mode_name, bindings) in [
("read_only", &defaults.read_only),
("edit", &defaults.edit),
("global", &defaults.global),
] {
if bindings.is_empty() {
continue;
}
template.push_str(&format!("[keybindings.{}]\n", mode_name));
// Get mode registry for categorization
if let Some(mode_registry) = registry.get_mode_registry(mode_name) {
// Required actions first
let mut found_required = false;
for (action_name, keybindings) in bindings {
if mode_registry.required.contains_key(action_name) {
if !found_required {
template.push_str("# Required\n");
found_required = true;
}
template.push_str(&format!("{} = {:?}\n", action_name, keybindings));
}
}
// Optional actions second
let mut found_optional = false;
for (action_name, keybindings) in bindings {
if mode_registry.optional.contains_key(action_name) {
if !found_optional {
template.push_str("# Optional\n");
found_optional = true;
}
template.push_str(&format!("{} = {:?}\n", action_name, keybindings));
}
}
} else {
// Fallback: just list all actions
for (action_name, keybindings) in bindings {
template.push_str(&format!("{} = {:?}\n", action_name, keybindings));
}
}
template.push('\n');
}
template
}
/// NEW: Generate a clean, minimal configuration template
/// Generate config that only contains actions different from defaults
/// Useful for minimal user configs
pub fn generate_minimal_template() -> String {
let defaults = CanvasKeybindings::with_vim_defaults();
let mut template = String::new();
template.push_str("# Minimal Canvas Configuration\n");
template.push_str("# Only uncomment and modify the keybindings you want to change\n");
template.push_str("# All other actions will use their default vim-style keybindings\n\n");
for (mode_name, bindings) in [
("read_only", &defaults.read_only),
("edit", &defaults.edit),
("global", &defaults.global),
] {
if bindings.is_empty() {
continue;
}
template.push_str(&format!("# [keybindings.{}]\n", mode_name));
for (action_name, keybindings) in bindings {
template.push_str(&format!("# {} = {:?}\n", action_name, keybindings));
}
template.push('\n');
}
template
}
/// Generate template from actual handler capabilities (legacy method for compatibility)
pub fn generate_template() -> String {
Self::generate_complete_template()
}
/// Generate clean template from actual handler capabilities (legacy method for compatibility)
pub fn generate_clean_template() -> String {
let registry = ActionRegistry::new();
let registry = ActionRegistry::from_handlers();
// Validate handlers first
if let Err(errors) = registry.validate_against_implementation() {
for error in errors {
eprintln!(" - {}", error);
}
}
registry.generate_clean_template()
}
/// NEW: Validate current configuration
/// Validate current configuration against actual implementation
pub fn validate(&self) -> ValidationResult {
let validator = ConfigValidator::new();
let registry = ActionRegistry::from_handlers();
let validator = ConfigValidator::new(registry);
validator.validate_keybindings(&self.keybindings)
}
/// NEW: Print validation results for current config
/// Print validation results for current config
pub fn print_validation(&self) {
let validator = ConfigValidator::new();
let registry = ActionRegistry::from_handlers();
let validator = ConfigValidator::new(registry);
let result = validator.validate_keybindings(&self.keybindings);
validator.print_validation_result(&result);
}
/// NEW: Generate config for missing required actions
pub fn generate_missing_config(&self) -> String {
let validator = ConfigValidator::new();
validator.generate_missing_config(&self.keybindings)
}
/// Load from TOML string
pub fn from_toml(toml_str: &str) -> Result<Self> {
toml::from_str(toml_str)
.with_context(|| "Failed to parse canvas config TOML")
.context("Failed to parse TOML configuration")
}
/// Load from file
pub fn from_file(path: &std::path::Path) -> Result<Self> {
let contents = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read config file: {:?}", path))?;
.context("Failed to read config file")?;
Self::from_toml(&contents)
}
/// NEW: Check if autocomplete should auto-trigger (simple logic)
/// Check if autocomplete should auto-trigger (simple logic)
pub fn should_auto_trigger_autocomplete(&self) -> bool {
// If trigger_autocomplete keybinding exists anywhere, use manual mode only
// If no trigger_autocomplete keybinding, use auto-trigger mode
!self.has_trigger_autocomplete_keybinding()
}
/// NEW: Check if user has configured manual trigger keybinding
/// Check if user has configured manual trigger keybinding
pub fn has_trigger_autocomplete_keybinding(&self) -> bool {
self.keybindings.edit.contains_key("trigger_autocomplete") ||
self.keybindings.read_only.contains_key("trigger_autocomplete") ||
self.keybindings.global.contains_key("trigger_autocomplete")
}
// ... rest of your existing methods stay the same ...
/// Get action for key in read-only mode
pub fn get_read_only_action(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> {
self.get_action_in_mode(&self.keybindings.read_only, key, modifiers)
@@ -277,21 +420,9 @@ impl CanvasConfig {
.or_else(|| self.get_action_in_mode(&self.keybindings.global, key, modifiers))
}
/// Get action for key in suggestions mode
pub fn get_suggestion_action(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> {
self.get_action_in_mode(&self.keybindings.suggestions, key, modifiers)
}
/// Get action for key (mode-aware)
pub fn get_action_for_key(&self, key: KeyCode, modifiers: KeyModifiers, is_edit_mode: bool, has_suggestions: bool) -> Option<&str> {
// Suggestions take priority when active
if has_suggestions {
if let Some(action) = self.get_suggestion_action(key, modifiers) {
return Some(action);
}
}
// Then check mode-specific
pub fn get_action_for_key(&self, key: KeyCode, modifiers: KeyModifiers, is_edit_mode: bool, _has_suggestions: bool) -> Option<&str> {
// Check mode-specific
if is_edit_mode {
self.get_edit_action(key, modifiers)
} else {
@@ -299,7 +430,6 @@ impl CanvasConfig {
}
}
// ... keep all your existing private methods ...
fn get_action_in_mode<'a>(&self, mode_bindings: &'a HashMap<String, Vec<String>>, key: KeyCode, modifiers: KeyModifiers) -> Option<&'a str> {
for (action, bindings) in mode_bindings {
for binding in bindings {
@@ -312,52 +442,224 @@ impl CanvasConfig {
}
fn matches_keybinding(&self, binding: &str, key: KeyCode, modifiers: KeyModifiers) -> bool {
// ... keep all your existing key matching logic ...
// (This is a very long method, so I'm just indicating to keep it as-is)
// Your existing implementation here...
true // placeholder - use your actual implementation
}
// Special handling for shift+character combinations
if binding.to_lowercase().starts_with("shift+") {
let parts: Vec<&str> = binding.split('+').collect();
if parts.len() == 2 && parts[1].len() == 1 {
let expected_lowercase = parts[1].chars().next().unwrap().to_lowercase().next().unwrap();
let expected_uppercase = expected_lowercase.to_uppercase().next().unwrap();
if let KeyCode::Char(actual_char) = key {
if actual_char == expected_uppercase && modifiers.contains(KeyModifiers::SHIFT) {
return true;
}
}
}
}
/// Convenience method to create vim preset
pub fn vim_preset() -> Self {
Self {
keybindings: CanvasKeybindings::with_vim_defaults(),
behavior: CanvasBehavior::default(),
appearance: CanvasAppearance::default(),
// Handle Shift+Tab -> BackTab
if binding.to_lowercase() == "shift+tab" && key == KeyCode::BackTab && modifiers.is_empty() {
return true;
}
}
/// Convenience method to create emacs preset
pub fn emacs_preset() -> Self {
Self {
keybindings: CanvasKeybindings::with_emacs_defaults(),
behavior: CanvasBehavior::default(),
appearance: CanvasAppearance::default(),
}
}
// Handle multi-character bindings (all standard keys without modifiers)
if binding.len() > 1 && !binding.contains('+') {
return match binding.to_lowercase().as_str() {
// Navigation keys
"left" => key == KeyCode::Left,
"right" => key == KeyCode::Right,
"up" => key == KeyCode::Up,
"down" => key == KeyCode::Down,
"home" => key == KeyCode::Home,
"end" => key == KeyCode::End,
"pageup" | "pgup" => key == KeyCode::PageUp,
"pagedown" | "pgdn" => key == KeyCode::PageDown,
/// Debug method to print loaded keybindings
pub fn debug_keybindings(&self) {
println!("📋 Canvas keybindings loaded:");
println!(" Read-only: {} actions", self.keybindings.read_only.len());
println!(" Edit: {} actions", self.keybindings.edit.len());
println!(" Suggestions: {} actions", self.keybindings.suggestions.len());
println!(" Global: {} actions", self.keybindings.global.len());
// NEW: Show validation status
let validation = self.validate();
if validation.is_valid {
println!(" ✅ Configuration is valid");
} else {
println!(" ❌ Configuration has {} errors", validation.errors.len());
// Editing keys
"insert" | "ins" => key == KeyCode::Insert,
"delete" | "del" => key == KeyCode::Delete,
"backspace" => key == KeyCode::Backspace,
// Tab keys
"tab" => key == KeyCode::Tab,
"backtab" => key == KeyCode::BackTab,
// Special keys
"enter" | "return" => key == KeyCode::Enter,
"escape" | "esc" => key == KeyCode::Esc,
"space" => key == KeyCode::Char(' '),
// Function keys F1-F24
"f1" => key == KeyCode::F(1),
"f2" => key == KeyCode::F(2),
"f3" => key == KeyCode::F(3),
"f4" => key == KeyCode::F(4),
"f5" => key == KeyCode::F(5),
"f6" => key == KeyCode::F(6),
"f7" => key == KeyCode::F(7),
"f8" => key == KeyCode::F(8),
"f9" => key == KeyCode::F(9),
"f10" => key == KeyCode::F(10),
"f11" => key == KeyCode::F(11),
"f12" => key == KeyCode::F(12),
"f13" => key == KeyCode::F(13),
"f14" => key == KeyCode::F(14),
"f15" => key == KeyCode::F(15),
"f16" => key == KeyCode::F(16),
"f17" => key == KeyCode::F(17),
"f18" => key == KeyCode::F(18),
"f19" => key == KeyCode::F(19),
"f20" => key == KeyCode::F(20),
"f21" => key == KeyCode::F(21),
"f22" => key == KeyCode::F(22),
"f23" => key == KeyCode::F(23),
"f24" => key == KeyCode::F(24),
// Lock keys (may not work reliably in all terminals)
"capslock" => key == KeyCode::CapsLock,
"scrolllock" => key == KeyCode::ScrollLock,
"numlock" => key == KeyCode::NumLock,
// System keys
"printscreen" => key == KeyCode::PrintScreen,
"pause" => key == KeyCode::Pause,
"menu" => key == KeyCode::Menu,
"keypadbegin" => key == KeyCode::KeypadBegin,
// Media keys (rarely supported but included for completeness)
"mediaplay" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Play),
"mediapause" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Pause),
"mediaplaypause" => key == KeyCode::Media(crossterm::event::MediaKeyCode::PlayPause),
"mediareverse" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Reverse),
"mediastop" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Stop),
"mediafastforward" => key == KeyCode::Media(crossterm::event::MediaKeyCode::FastForward),
"mediarewind" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Rewind),
"mediatracknext" => key == KeyCode::Media(crossterm::event::MediaKeyCode::TrackNext),
"mediatrackprevious" => key == KeyCode::Media(crossterm::event::MediaKeyCode::TrackPrevious),
"mediarecord" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Record),
"medialowervolume" => key == KeyCode::Media(crossterm::event::MediaKeyCode::LowerVolume),
"mediaraisevolume" => key == KeyCode::Media(crossterm::event::MediaKeyCode::RaiseVolume),
"mediamutevolume" => key == KeyCode::Media(crossterm::event::MediaKeyCode::MuteVolume),
// Modifier keys (these work better as part of combinations)
"leftshift" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::LeftShift),
"leftcontrol" | "leftctrl" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::LeftControl),
"leftalt" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::LeftAlt),
"leftsuper" | "leftwindows" | "leftcmd" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::LeftSuper),
"lefthyper" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::LeftHyper),
"leftmeta" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::LeftMeta),
"rightshift" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::RightShift),
"rightcontrol" | "rightctrl" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::RightControl),
"rightalt" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::RightAlt),
"rightsuper" | "rightwindows" | "rightcmd" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::RightSuper),
"righthyper" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::RightHyper),
"rightmeta" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::RightMeta),
"isolevel3shift" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::IsoLevel3Shift),
"isolevel5shift" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::IsoLevel5Shift),
// Multi-key sequences need special handling
"gg" => false, // This needs sequence handling
_ => {
// Handle single characters and punctuation
if binding.len() == 1 {
if let Some(c) = binding.chars().next() {
key == KeyCode::Char(c)
} else {
false
}
} else {
false
}
}
};
}
if !validation.warnings.is_empty() {
println!(" ⚠️ Configuration has {} warnings", validation.warnings.len());
// Handle modifier combinations (like "Ctrl+F5", "Alt+Shift+A")
let parts: Vec<&str> = binding.split('+').collect();
let mut expected_modifiers = KeyModifiers::empty();
let mut expected_key = None;
for part in parts {
match part.to_lowercase().as_str() {
// Modifiers
"ctrl" | "control" => expected_modifiers |= KeyModifiers::CONTROL,
"shift" => expected_modifiers |= KeyModifiers::SHIFT,
"alt" => expected_modifiers |= KeyModifiers::ALT,
"super" | "windows" | "cmd" => expected_modifiers |= KeyModifiers::SUPER,
"hyper" => expected_modifiers |= KeyModifiers::HYPER,
"meta" => expected_modifiers |= KeyModifiers::META,
// Navigation keys
"left" => expected_key = Some(KeyCode::Left),
"right" => expected_key = Some(KeyCode::Right),
"up" => expected_key = Some(KeyCode::Up),
"down" => expected_key = Some(KeyCode::Down),
"home" => expected_key = Some(KeyCode::Home),
"end" => expected_key = Some(KeyCode::End),
"pageup" | "pgup" => expected_key = Some(KeyCode::PageUp),
"pagedown" | "pgdn" => expected_key = Some(KeyCode::PageDown),
// Editing keys
"insert" | "ins" => expected_key = Some(KeyCode::Insert),
"delete" | "del" => expected_key = Some(KeyCode::Delete),
"backspace" => expected_key = Some(KeyCode::Backspace),
// Tab keys
"tab" => expected_key = Some(KeyCode::Tab),
"backtab" => expected_key = Some(KeyCode::BackTab),
// Special keys
"enter" | "return" => expected_key = Some(KeyCode::Enter),
"escape" | "esc" => expected_key = Some(KeyCode::Esc),
"space" => expected_key = Some(KeyCode::Char(' ')),
// Function keys
"f1" => expected_key = Some(KeyCode::F(1)),
"f2" => expected_key = Some(KeyCode::F(2)),
"f3" => expected_key = Some(KeyCode::F(3)),
"f4" => expected_key = Some(KeyCode::F(4)),
"f5" => expected_key = Some(KeyCode::F(5)),
"f6" => expected_key = Some(KeyCode::F(6)),
"f7" => expected_key = Some(KeyCode::F(7)),
"f8" => expected_key = Some(KeyCode::F(8)),
"f9" => expected_key = Some(KeyCode::F(9)),
"f10" => expected_key = Some(KeyCode::F(10)),
"f11" => expected_key = Some(KeyCode::F(11)),
"f12" => expected_key = Some(KeyCode::F(12)),
"f13" => expected_key = Some(KeyCode::F(13)),
"f14" => expected_key = Some(KeyCode::F(14)),
"f15" => expected_key = Some(KeyCode::F(15)),
"f16" => expected_key = Some(KeyCode::F(16)),
"f17" => expected_key = Some(KeyCode::F(17)),
"f18" => expected_key = Some(KeyCode::F(18)),
"f19" => expected_key = Some(KeyCode::F(19)),
"f20" => expected_key = Some(KeyCode::F(20)),
"f21" => expected_key = Some(KeyCode::F(21)),
"f22" => expected_key = Some(KeyCode::F(22)),
"f23" => expected_key = Some(KeyCode::F(23)),
"f24" => expected_key = Some(KeyCode::F(24)),
// Lock keys
"capslock" => expected_key = Some(KeyCode::CapsLock),
"scrolllock" => expected_key = Some(KeyCode::ScrollLock),
"numlock" => expected_key = Some(KeyCode::NumLock),
// System keys
"printscreen" => expected_key = Some(KeyCode::PrintScreen),
"pause" => expected_key = Some(KeyCode::Pause),
"menu" => expected_key = Some(KeyCode::Menu),
"keypadbegin" => expected_key = Some(KeyCode::KeypadBegin),
// Single character (letters, numbers, punctuation)
part => {
if part.len() == 1 {
if let Some(c) = part.chars().next() {
expected_key = Some(KeyCode::Char(c));
}
}
}
}
}
modifiers == expected_modifiers && Some(key) == expected_key
}
}
// Re-export for convenience
pub use crate::canvas::actions::CanvasAction;
pub use crate::dispatcher::ActionDispatcher;

View File

@@ -0,0 +1,93 @@
// src/config/introspection.rs
//! Handler capability introspection system
//!
//! This module provides traits and utilities for handlers to report their capabilities,
//! enabling automatic configuration generation and validation.
use std::collections::HashMap;
/// Specification for a single action that a handler can perform
#[derive(Debug, Clone)]
pub struct ActionSpec {
/// Action name (e.g., "move_left", "delete_char_backward")
pub name: String,
/// Human-readable description of what this action does
pub description: String,
/// Example keybindings for this action (e.g., ["Left", "h"])
pub examples: Vec<String>,
/// Whether this action is required for the handler to function properly
pub is_required: bool,
}
/// Complete capability description for a single handler
#[derive(Debug, Clone)]
pub struct HandlerCapabilities {
/// Mode name this handler operates in (e.g., "edit", "read_only")
pub mode_name: String,
/// All actions this handler can perform
pub actions: Vec<ActionSpec>,
/// Actions handled automatically without configuration (e.g., "insert_char")
pub auto_handled: Vec<String>,
}
/// Trait that handlers implement to report their capabilities
///
/// This enables the configuration system to automatically discover what actions
/// are available and validate user configurations against actual implementations.
pub trait ActionHandlerIntrospection {
/// Return complete capability information for this handler
fn introspect() -> HandlerCapabilities;
/// Validate that this handler actually supports its claimed actions
/// Override this to add custom validation logic
fn validate_capabilities() -> Result<(), String> {
Ok(()) // Default: assume handler is valid
}
}
/// Discovers capabilities from all registered handlers
pub struct HandlerDiscovery;
impl HandlerDiscovery {
/// Discover capabilities from all known handlers
/// Add new handlers to this function as they are created
pub fn discover_all() -> HashMap<String, HandlerCapabilities> {
let mut capabilities = HashMap::new();
// Register all known handlers here
let edit_caps = crate::canvas::actions::handlers::edit::EditHandler::introspect();
capabilities.insert("edit".to_string(), edit_caps);
let readonly_caps = crate::canvas::actions::handlers::readonly::ReadOnlyHandler::introspect();
capabilities.insert("read_only".to_string(), readonly_caps);
let highlight_caps = crate::canvas::actions::handlers::highlight::HighlightHandler::introspect();
capabilities.insert("highlight".to_string(), highlight_caps);
capabilities
}
/// Validate all handlers support their claimed capabilities
pub fn validate_all_handlers() -> Result<(), Vec<String>> {
let mut errors = Vec::new();
// Validate each handler
if let Err(e) = crate::canvas::actions::handlers::edit::EditHandler::validate_capabilities() {
errors.push(format!("Edit handler: {}", e));
}
if let Err(e) = crate::canvas::actions::handlers::readonly::ReadOnlyHandler::validate_capabilities() {
errors.push(format!("ReadOnly handler: {}", e));
}
if let Err(e) = crate::canvas::actions::handlers::highlight::HighlightHandler::validate_capabilities() {
errors.push(format!("Highlight handler: {}", e));
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}

View File

@@ -3,8 +3,10 @@
mod registry;
mod config;
mod validation;
pub mod introspection;
// Re-export everything from the main config module
pub use registry::*;
pub use validation::*;
pub use config::*;
pub use introspection::*;

View File

@@ -1,357 +1,67 @@
// src/config/registry.rs
use std::collections::HashMap;
use crate::canvas::modes::AppMode;
#[derive(Debug, Clone)]
pub struct ActionSpec {
pub name: String,
pub description: String,
pub examples: Vec<String>,
pub mode_specific: bool, // true if different behavior per mode
}
use crate::config::introspection::{HandlerDiscovery, ActionSpec, HandlerCapabilities};
#[derive(Debug, Clone)]
pub struct ModeRegistry {
pub required: HashMap<String, ActionSpec>,
pub optional: HashMap<String, ActionSpec>,
pub auto_handled: Vec<String>, // Never appear in config
pub auto_handled: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct ActionRegistry {
pub edit_mode: ModeRegistry,
pub readonly_mode: ModeRegistry,
pub suggestions: ModeRegistry,
pub global: ModeRegistry,
pub modes: HashMap<String, ModeRegistry>,
}
impl ActionRegistry {
pub fn new() -> Self {
Self {
edit_mode: Self::edit_mode_registry(),
readonly_mode: Self::readonly_mode_registry(),
suggestions: Self::suggestions_registry(),
global: Self::global_registry(),
}
}
fn edit_mode_registry() -> ModeRegistry {
let mut required = HashMap::new();
let mut optional = HashMap::new();
// REQUIRED - These MUST be configured
required.insert("move_left".to_string(), ActionSpec {
name: "move_left".to_string(),
description: "Move cursor one position to the left".to_string(),
examples: vec!["Left".to_string(), "h".to_string()],
mode_specific: false,
});
/// NEW: Create registry by discovering actual handler capabilities
pub fn from_handlers() -> Self {
let handler_capabilities = HandlerDiscovery::discover_all();
let mut modes = HashMap::new();
required.insert("move_right".to_string(), ActionSpec {
name: "move_right".to_string(),
description: "Move cursor one position to the right".to_string(),
examples: vec!["Right".to_string(), "l".to_string()],
mode_specific: false,
});
required.insert("move_up".to_string(), ActionSpec {
name: "move_up".to_string(),
description: "Move to previous field or line".to_string(),
examples: vec!["Up".to_string(), "k".to_string()],
mode_specific: false,
});
required.insert("move_down".to_string(), ActionSpec {
name: "move_down".to_string(),
description: "Move to next field or line".to_string(),
examples: vec!["Down".to_string(), "j".to_string()],
mode_specific: false,
});
required.insert("delete_char_backward".to_string(), ActionSpec {
name: "delete_char_backward".to_string(),
description: "Delete character before cursor".to_string(),
examples: vec!["Backspace".to_string()],
mode_specific: false,
});
required.insert("next_field".to_string(), ActionSpec {
name: "next_field".to_string(),
description: "Move to next input field".to_string(),
examples: vec!["Tab".to_string(), "Enter".to_string()],
mode_specific: false,
});
required.insert("prev_field".to_string(), ActionSpec {
name: "prev_field".to_string(),
description: "Move to previous input field".to_string(),
examples: vec!["Shift+Tab".to_string()],
mode_specific: false,
});
// OPTIONAL - These can be configured or omitted
optional.insert("move_word_next".to_string(), ActionSpec {
name: "move_word_next".to_string(),
description: "Move cursor to start of next word".to_string(),
examples: vec!["Ctrl+Right".to_string(), "w".to_string()],
mode_specific: false,
});
optional.insert("move_word_prev".to_string(), ActionSpec {
name: "move_word_prev".to_string(),
description: "Move cursor to start of previous word".to_string(),
examples: vec!["Ctrl+Left".to_string(), "b".to_string()],
mode_specific: false,
});
optional.insert("move_word_end".to_string(), ActionSpec {
name: "move_word_end".to_string(),
description: "Move cursor to end of current/next word".to_string(),
examples: vec!["e".to_string()],
mode_specific: false,
});
optional.insert("move_word_end_prev".to_string(), ActionSpec {
name: "move_word_end_prev".to_string(),
description: "Move cursor to end of previous word".to_string(),
examples: vec!["ge".to_string()],
mode_specific: false,
});
optional.insert("move_line_start".to_string(), ActionSpec {
name: "move_line_start".to_string(),
description: "Move cursor to beginning of line".to_string(),
examples: vec!["Home".to_string(), "0".to_string()],
mode_specific: false,
});
optional.insert("move_line_end".to_string(), ActionSpec {
name: "move_line_end".to_string(),
description: "Move cursor to end of line".to_string(),
examples: vec!["End".to_string(), "$".to_string()],
mode_specific: false,
});
optional.insert("move_first_line".to_string(), ActionSpec {
name: "move_first_line".to_string(),
description: "Move to first field".to_string(),
examples: vec!["Ctrl+Home".to_string(), "gg".to_string()],
mode_specific: false,
});
optional.insert("move_last_line".to_string(), ActionSpec {
name: "move_last_line".to_string(),
description: "Move to last field".to_string(),
examples: vec!["Ctrl+End".to_string(), "G".to_string()],
mode_specific: false,
});
optional.insert("delete_char_forward".to_string(), ActionSpec {
name: "delete_char_forward".to_string(),
description: "Delete character after cursor".to_string(),
examples: vec!["Delete".to_string()],
mode_specific: false,
});
ModeRegistry {
required,
optional,
auto_handled: vec![
"insert_char".to_string(), // Any printable character
],
for (mode_name, capabilities) in handler_capabilities {
let mode_registry = Self::build_mode_registry(capabilities);
modes.insert(mode_name, mode_registry);
}
}
fn readonly_mode_registry() -> ModeRegistry {
let mut required = HashMap::new();
let mut optional = HashMap::new();
// REQUIRED - Navigation is essential in read-only mode
required.insert("move_left".to_string(), ActionSpec {
name: "move_left".to_string(),
description: "Move cursor one position to the left".to_string(),
examples: vec!["h".to_string(), "Left".to_string()],
mode_specific: true,
});
required.insert("move_right".to_string(), ActionSpec {
name: "move_right".to_string(),
description: "Move cursor one position to the right".to_string(),
examples: vec!["l".to_string(), "Right".to_string()],
mode_specific: true,
});
required.insert("move_up".to_string(), ActionSpec {
name: "move_up".to_string(),
description: "Move to previous field".to_string(),
examples: vec!["k".to_string(), "Up".to_string()],
mode_specific: true,
});
required.insert("move_down".to_string(), ActionSpec {
name: "move_down".to_string(),
description: "Move to next field".to_string(),
examples: vec!["j".to_string(), "Down".to_string()],
mode_specific: true,
});
// OPTIONAL - Advanced navigation
optional.insert("move_word_next".to_string(), ActionSpec {
name: "move_word_next".to_string(),
description: "Move cursor to start of next word".to_string(),
examples: vec!["w".to_string()],
mode_specific: true,
});
optional.insert("move_word_prev".to_string(), ActionSpec {
name: "move_word_prev".to_string(),
description: "Move cursor to start of previous word".to_string(),
examples: vec!["b".to_string()],
mode_specific: true,
});
optional.insert("move_word_end".to_string(), ActionSpec {
name: "move_word_end".to_string(),
description: "Move cursor to end of current/next word".to_string(),
examples: vec!["e".to_string()],
mode_specific: true,
});
optional.insert("move_word_end_prev".to_string(), ActionSpec {
name: "move_word_end_prev".to_string(),
description: "Move cursor to end of previous word".to_string(),
examples: vec!["ge".to_string()],
mode_specific: true,
});
optional.insert("move_line_start".to_string(), ActionSpec {
name: "move_line_start".to_string(),
description: "Move cursor to beginning of line".to_string(),
examples: vec!["0".to_string()],
mode_specific: true,
});
optional.insert("move_line_end".to_string(), ActionSpec {
name: "move_line_end".to_string(),
description: "Move cursor to end of line".to_string(),
examples: vec!["$".to_string()],
mode_specific: true,
});
optional.insert("move_first_line".to_string(), ActionSpec {
name: "move_first_line".to_string(),
description: "Move to first field".to_string(),
examples: vec!["gg".to_string()],
mode_specific: true,
});
optional.insert("move_last_line".to_string(), ActionSpec {
name: "move_last_line".to_string(),
description: "Move to last field".to_string(),
examples: vec!["G".to_string()],
mode_specific: true,
});
optional.insert("next_field".to_string(), ActionSpec {
name: "next_field".to_string(),
description: "Move to next input field".to_string(),
examples: vec!["Tab".to_string()],
mode_specific: true,
});
optional.insert("prev_field".to_string(), ActionSpec {
name: "prev_field".to_string(),
description: "Move to previous input field".to_string(),
examples: vec!["Shift+Tab".to_string()],
mode_specific: true,
});
ModeRegistry {
required,
optional,
auto_handled: vec![], // Read-only mode has no auto-handled actions
}
Self { modes }
}
fn suggestions_registry() -> ModeRegistry {
/// Build a mode registry from handler capabilities
fn build_mode_registry(capabilities: HandlerCapabilities) -> ModeRegistry {
let mut required = HashMap::new();
// REQUIRED - Essential for suggestion navigation
required.insert("suggestion_up".to_string(), ActionSpec {
name: "suggestion_up".to_string(),
description: "Move selection to previous suggestion".to_string(),
examples: vec!["Up".to_string(), "Ctrl+p".to_string()],
mode_specific: false,
});
required.insert("suggestion_down".to_string(), ActionSpec {
name: "suggestion_down".to_string(),
description: "Move selection to next suggestion".to_string(),
examples: vec!["Down".to_string(), "Ctrl+n".to_string()],
mode_specific: false,
});
required.insert("select_suggestion".to_string(), ActionSpec {
name: "select_suggestion".to_string(),
description: "Select the currently highlighted suggestion".to_string(),
examples: vec!["Enter".to_string(), "Tab".to_string()],
mode_specific: false,
});
required.insert("exit_suggestions".to_string(), ActionSpec {
name: "exit_suggestions".to_string(),
description: "Close suggestions without selecting".to_string(),
examples: vec!["Esc".to_string()],
mode_specific: false,
});
let mut optional = HashMap::new();
for action_spec in capabilities.actions {
if action_spec.is_required {
required.insert(action_spec.name.clone(), action_spec);
} else {
optional.insert(action_spec.name.clone(), action_spec);
}
}
ModeRegistry {
required,
optional: HashMap::new(),
auto_handled: vec![],
}
}
fn global_registry() -> ModeRegistry {
let mut optional = HashMap::new();
// OPTIONAL - Global overrides
optional.insert("move_up".to_string(), ActionSpec {
name: "move_up".to_string(),
description: "Global override for up movement".to_string(),
examples: vec!["Up".to_string()],
mode_specific: false,
});
optional.insert("move_down".to_string(), ActionSpec {
name: "move_down".to_string(),
description: "Global override for down movement".to_string(),
examples: vec!["Down".to_string()],
mode_specific: false,
});
ModeRegistry {
required: HashMap::new(),
optional,
auto_handled: vec![],
auto_handled: capabilities.auto_handled,
}
}
pub fn get_mode_registry(&self, mode: &str) -> &ModeRegistry {
match mode {
"edit" => &self.edit_mode,
"read_only" => &self.readonly_mode,
"suggestions" => &self.suggestions,
"global" => &self.global,
_ => &self.global, // fallback
}
/// Validate that the registry matches the actual implementation
pub fn validate_against_implementation(&self) -> Result<(), Vec<String>> {
HandlerDiscovery::validate_all_handlers()
}
pub fn get_mode_registry(&self, mode: &str) -> Option<&ModeRegistry> {
self.modes.get(mode)
}
pub fn all_known_actions(&self) -> Vec<String> {
let mut actions = Vec::new();
for registry in [&self.edit_mode, &self.readonly_mode, &self.suggestions, &self.global] {
for registry in self.modes.values() {
actions.extend(registry.required.keys().cloned());
actions.extend(registry.optional.keys().cloned());
}
@@ -364,39 +74,34 @@ impl ActionRegistry {
pub fn generate_config_template(&self) -> String {
let mut template = String::new();
template.push_str("# Canvas Library Configuration Template\n");
template.push_str("# Generated automatically - customize as needed\n\n");
template.push_str("# Generated automatically from actual handler capabilities\n\n");
template.push_str("[keybindings.edit]\n");
template.push_str("# REQUIRED ACTIONS - These must be configured\n");
for (name, spec) in &self.edit_mode.required {
template.push_str(&format!("# {}\n", spec.description));
template.push_str(&format!("{} = {:?}\n\n", name, spec.examples));
}
template.push_str("# OPTIONAL ACTIONS - Configure these if you want them enabled\n");
for (name, spec) in &self.edit_mode.optional {
template.push_str(&format!("# {}\n", spec.description));
template.push_str(&format!("# {} = {:?}\n\n", name, spec.examples));
}
template.push_str("[keybindings.read_only]\n");
template.push_str("# REQUIRED ACTIONS - These must be configured\n");
for (name, spec) in &self.readonly_mode.required {
template.push_str(&format!("# {}\n", spec.description));
template.push_str(&format!("{} = {:?}\n\n", name, spec.examples));
}
template.push_str("# OPTIONAL ACTIONS - Configure these if you want them enabled\n");
for (name, spec) in &self.readonly_mode.optional {
template.push_str(&format!("# {}\n", spec.description));
template.push_str(&format!("# {} = {:?}\n\n", name, spec.examples));
}
template.push_str("[keybindings.suggestions]\n");
template.push_str("# REQUIRED ACTIONS - These must be configured\n");
for (name, spec) in &self.suggestions.required {
template.push_str(&format!("# {}\n", spec.description));
template.push_str(&format!("{} = {:?}\n\n", name, spec.examples));
for (mode_name, registry) in &self.modes {
template.push_str(&format!("[keybindings.{}]\n", mode_name));
if !registry.required.is_empty() {
template.push_str("# REQUIRED ACTIONS - These must be configured\n");
for (name, spec) in &registry.required {
template.push_str(&format!("# {}\n", spec.description));
template.push_str(&format!("{} = {:?}\n\n", name, spec.examples));
}
}
if !registry.optional.is_empty() {
template.push_str("# OPTIONAL ACTIONS - Configure these if you want them enabled\n");
for (name, spec) in &registry.optional {
template.push_str(&format!("# {}\n", spec.description));
template.push_str(&format!("# {} = {:?}\n\n", name, spec.examples));
}
}
if !registry.auto_handled.is_empty() {
template.push_str("# AUTO-HANDLED - These are handled automatically, don't configure:\n");
for auto_action in &registry.auto_handled {
template.push_str(&format!("# {} (automatic)\n", auto_action));
}
template.push('\n');
}
}
template
@@ -405,45 +110,24 @@ impl ActionRegistry {
pub fn generate_clean_template(&self) -> String {
let mut template = String::new();
// Edit Mode
template.push_str("[keybindings.edit]\n");
template.push_str("# Required\n");
for (name, spec) in &self.edit_mode.required {
template.push_str(&format!("{} = {:?}\n", name, spec.examples));
}
template.push_str("# Optional\n");
for (name, spec) in &self.edit_mode.optional {
template.push_str(&format!("{} = {:?}\n", name, spec.examples));
}
template.push('\n');
// Read-Only Mode
template.push_str("[keybindings.read_only]\n");
template.push_str("# Required\n");
for (name, spec) in &self.readonly_mode.required {
template.push_str(&format!("{} = {:?}\n", name, spec.examples));
}
template.push_str("# Optional\n");
for (name, spec) in &self.readonly_mode.optional {
template.push_str(&format!("{} = {:?}\n", name, spec.examples));
}
template.push('\n');
// Suggestions Mode
template.push_str("[keybindings.suggestions]\n");
template.push_str("# Required\n");
for (name, spec) in &self.suggestions.required {
template.push_str(&format!("{} = {:?}\n", name, spec.examples));
}
template.push('\n');
// Global (all optional)
if !self.global.optional.is_empty() {
template.push_str("[keybindings.global]\n");
template.push_str("# Optional\n");
for (name, spec) in &self.global.optional {
template.push_str(&format!("{} = {:?}\n", name, spec.examples));
for (mode_name, registry) in &self.modes {
template.push_str(&format!("[keybindings.{}]\n", mode_name));
if !registry.required.is_empty() {
template.push_str("# Required\n");
for (name, spec) in &registry.required {
template.push_str(&format!("{} = {:?}\n", name, spec.examples));
}
}
if !registry.optional.is_empty() {
template.push_str("# Optional\n");
for (name, spec) in &registry.optional {
template.push_str(&format!("{} = {:?}\n", name, spec.examples));
}
}
template.push('\n');
}
template

View File

@@ -8,19 +8,19 @@ use crate::config::CanvasKeybindings;
#[derive(Error, Debug)]
pub enum ValidationError {
#[error("Missing required action '{action}' in {mode} mode")]
MissingRequired {
action: String,
MissingRequired {
action: String,
mode: String,
suggestion: String,
},
#[error("Unknown action '{action}' in {mode} mode")]
UnknownAction {
action: String,
UnknownAction {
action: String,
mode: String,
similar: Vec<String>,
},
#[error("Multiple validation errors")]
Multiple(Vec<ValidationError>),
}
@@ -70,47 +70,46 @@ pub struct ConfigValidator {
}
impl ConfigValidator {
pub fn new() -> Self {
// FIXED: Accept registry parameter to match config.rs calls
pub fn new(registry: ActionRegistry) -> Self {
Self {
registry: ActionRegistry::new(),
registry,
}
}
pub fn validate_keybindings(&self, keybindings: &CanvasKeybindings) -> ValidationResult {
let mut result = ValidationResult::new();
// Validate each mode
result.merge(self.validate_mode_bindings(
"edit",
&keybindings.edit,
self.registry.get_mode_registry("edit")
));
// Validate each mode that exists in the registry
if let Some(edit_registry) = self.registry.get_mode_registry("edit") {
result.merge(self.validate_mode_bindings(
"edit",
&keybindings.edit,
edit_registry
));
}
result.merge(self.validate_mode_bindings(
"read_only",
&keybindings.read_only,
self.registry.get_mode_registry("read_only")
));
if let Some(readonly_registry) = self.registry.get_mode_registry("read_only") {
result.merge(self.validate_mode_bindings(
"read_only",
&keybindings.read_only,
readonly_registry
));
}
result.merge(self.validate_mode_bindings(
"suggestions",
&keybindings.suggestions,
self.registry.get_mode_registry("suggestions")
));
// Skip suggestions mode if not discovered by introspection
// (autocomplete is separate concern as requested)
result.merge(self.validate_mode_bindings(
"global",
&keybindings.global,
self.registry.get_mode_registry("global")
));
// Skip global mode if not discovered by introspection
// (can be added later if needed)
result
}
fn validate_mode_bindings(
&self,
mode_name: &str,
bindings: &HashMap<String, Vec<String>>,
&self,
mode_name: &str,
bindings: &HashMap<String, Vec<String>>,
registry: &ModeRegistry
) -> ValidationResult {
let mut result = ValidationResult::new();
@@ -122,8 +121,8 @@ impl ConfigValidator {
action: action_name.clone(),
mode: mode_name.to_string(),
suggestion: format!(
"Add to config: {} = {:?}",
action_name,
"Add to config: {} = {:?}",
action_name,
spec.examples
),
});
@@ -151,7 +150,7 @@ impl ConfigValidator {
if key_list.is_empty() {
result.add_warning(ValidationWarning {
message: format!(
"Action '{}' in {} mode has empty keybinding list",
"Action '{}' in {} mode has empty keybinding list",
action_name, mode_name
),
suggestion: Some(format!(
@@ -166,11 +165,11 @@ impl ConfigValidator {
if bindings.contains_key(auto_action) {
result.add_warning(ValidationWarning {
message: format!(
"Action '{}' in {} mode is auto-handled and shouldn't be in config",
"Action '{}' in {} mode is auto-handled and shouldn't be in config",
auto_action, mode_name
),
suggestion: Some(format!(
"Remove '{}' from config - it's handled automatically",
"Remove '{}' from config - it's handled automatically",
auto_action
)),
});
@@ -182,7 +181,7 @@ impl ConfigValidator {
fn find_similar_actions(&self, action: &str, known_actions: &std::collections::HashSet<&String>) -> Vec<String> {
let mut similar = Vec::new();
for known in known_actions {
if self.is_similar(action, known) {
similar.push(known.to_string());
@@ -198,7 +197,7 @@ impl ConfigValidator {
// Simple similarity check - could be improved with proper edit distance
let a_lower = a.to_lowercase();
let b_lower = b.to_lowercase();
// Check if one contains the other
if a_lower.contains(&b_lower) || b_lower.contains(&a_lower) {
return true;

55
canvas/view_docs.sh Executable file
View File

@@ -0,0 +1,55 @@
#!/bin/bash
# Enhanced documentation viewer for your canvas library
echo "=========================================="
echo "CANVAS LIBRARY DOCUMENTATION"
echo "=========================================="
# Function to display module docs with colors
show_module() {
local module=$1
local title=$2
echo -e "\n\033[1;34m=== $title ===\033[0m"
echo -e "\033[33mFiles in $module:\033[0m"
find src/$module -name "*.rs" 2>/dev/null | sort
echo
# Show doc comments for this module
find src/$module -name "*.rs" 2>/dev/null | while read file; do
if grep -q "///" "$file"; then
echo -e "\033[32m--- $file ---\033[0m"
grep -n "^\s*///" "$file" | sed 's/^\([0-9]*:\)\s*\/\/\/ /\1 /' | head -10
echo
fi
done
}
# Main modules
show_module "canvas" "CANVAS SYSTEM"
show_module "autocomplete" "AUTOCOMPLETE SYSTEM"
show_module "config" "CONFIGURATION SYSTEM"
# Show lib.rs and other root files
echo -e "\n\033[1;34m=== ROOT DOCUMENTATION ===\033[0m"
if [ -f "src/lib.rs" ]; then
echo -e "\033[32m--- src/lib.rs ---\033[0m"
grep -n "^\s*///" src/lib.rs | sed 's/^\([0-9]*:\)\s*\/\/\/ /\1 /' 2>/dev/null
fi
if [ -f "src/dispatcher.rs" ]; then
echo -e "\033[32m--- src/dispatcher.rs ---\033[0m"
grep -n "^\s*///" src/dispatcher.rs | sed 's/^\([0-9]*:\)\s*\/\/\/ /\1 /' 2>/dev/null
fi
echo -e "\n\033[1;36m=========================================="
echo "To view specific module documentation:"
echo " ./view_canvas_docs.sh canvas"
echo " ./view_canvas_docs.sh autocomplete"
echo " ./view_canvas_docs.sh config"
echo "==========================================\033[0m"
# If specific module requested
if [ $# -eq 1 ]; then
show_module "$1" "$(echo $1 | tr '[:lower:]' '[:upper:]') MODULE DETAILS"
fi

1
client/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
canvas_config.toml.txt

View File

@@ -1,58 +0,0 @@
# canvas_config.toml - Complete Canvas Configuration
[behavior]
wrap_around_fields = true
auto_save_on_field_change = false
word_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"
max_suggestions = 6
[appearance]
cursor_style = "block" # "block", "bar", "underline"
show_field_numbers = false
highlight_current_field = true
# Read-only mode keybindings (vim-style)
[keybindings.read_only]
move_left = ["h"]
move_right = ["l"]
move_up = ["k"]
move_down = ["j"]
move_word_next = ["w"]
move_word_end = ["e"]
move_word_prev = ["b"]
move_word_end_prev = ["ge"]
move_line_start = ["0"]
move_line_end = ["$"]
move_first_line = ["gg"]
move_last_line = ["shift+g"]
next_field = ["Tab"]
prev_field = ["Shift+Tab"]
# Edit mode keybindings
[keybindings.edit]
delete_char_backward = ["Backspace"]
delete_char_forward = ["Delete"]
move_left = ["Left"]
move_right = ["Right"]
move_up = ["Up"]
move_down = ["Down"]
move_line_start = ["Home"]
move_line_end = ["End"]
move_word_next = ["Ctrl+Right"]
move_word_prev = ["Ctrl+Left"]
next_field = ["Tab"]
prev_field = ["Shift+Tab"]
trigger_autocomplete = ["Ctrl+p"]
# Suggestion/autocomplete keybindings
[keybindings.suggestions]
suggestion_up = ["Up", "Ctrl+p"]
suggestion_down = ["Down", "Ctrl+n"]
select_suggestion = ["Enter", "Tab"]
exit_suggestions = ["Esc"]
trigger_autocomplete = ["Tab"]
# Global keybindings (work in both modes)
[keybindings.global]
move_up = ["Up"]
move_down = ["Down"]

View File

@@ -42,10 +42,42 @@ next_entry = ["right","1"]
enter_highlight_mode = ["v"]
enter_highlight_mode_linewise = ["ctrl+v"]
### AUTOGENERATED CANVAS CONFIG
# Required
move_up = ["k", "Up"]
move_left = ["h", "Left"]
move_right = ["l", "Right"]
move_down = ["j", "Down"]
# Optional
move_line_end = ["$"]
move_word_next = ["w"]
next_field = ["Tab"]
move_word_prev = ["b"]
move_word_end = ["e"]
move_last_line = ["shift+g"]
move_word_end_prev = ["ge"]
move_line_start = ["0"]
move_first_line = ["g+g"]
prev_field = ["Shift+Tab"]
[keybindings.highlight]
exit_highlight_mode = ["esc"]
enter_highlight_mode_linewise = ["ctrl+v"]
### AUTOGENERATED CANVAS CONFIG
# Required
move_left = ["h", "Left"]
move_right = ["l", "Right"]
move_up = ["k", "Up"]
move_down = ["j", "Down"]
# Optional
move_word_next = ["w"]
move_line_start = ["0"]
move_line_end = ["$"]
move_word_prev = ["b"]
move_word_end = ["e"]
[keybindings.edit]
# BIG CHANGES NOW EXIT HANDLES EITHER IF THOSE
# exit_edit_mode = ["esc","ctrl+e"]
@@ -53,13 +85,30 @@ enter_highlight_mode_linewise = ["ctrl+v"]
# select_suggestion = ["enter"]
# next_field = ["enter"]
enter_decider = ["enter"]
prev_field = ["shift+enter"]
exit = ["esc", "ctrl+e"]
delete_char_forward = ["delete"]
delete_char_backward = ["backspace"]
suggestion_down = ["ctrl+n", "tab"]
suggestion_up = ["ctrl+p", "shift+tab"]
### AUTOGENERATED CANVAS CONFIG
# Required
move_right = ["Right", "l"]
delete_char_backward = ["Backspace"]
next_field = ["Tab", "Enter"]
move_up = ["Up", "k"]
move_down = ["Down", "j"]
prev_field = ["Shift+Tab"]
move_left = ["Left", "h"]
# Optional
move_last_line = ["Ctrl+End", "G"]
delete_char_forward = ["Delete"]
move_word_prev = ["Ctrl+Left", "b"]
move_word_end = ["e"]
move_word_end_prev = ["ge"]
move_first_line = ["Ctrl+Home", "gg"]
move_word_next = ["Ctrl+Right", "w"]
move_line_start = ["Home", "0"]
move_line_end = ["End", "$"]
[keybindings.command]
exit_command_mode = ["ctrl+g", "esc"]
command_execute = ["enter"]
@@ -77,3 +126,9 @@ keybinding_mode = "vim" # Options: "default", "vim", "emacs"
[colors]
theme = "dark"
# Options: "light", "dark", "high_contrast"

View File

@@ -12,7 +12,7 @@ use canvas::canvas::CanvasState;
use canvas::{canvas::CanvasAction, dispatcher::ActionDispatcher, canvas::ActionResult};
use anyhow::Result;
use common::proto::komp_ac::search::search_response::Hit;
use crossterm::event::{KeyCode, KeyEvent};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use tokio::sync::mpsc;
use tracing::info;
@@ -143,23 +143,46 @@ async fn execute_canvas_action(
}
}
/// NEW: Unified canvas action handler for any CanvasState (LoginState, RegisterState, etc.)
/// This replaces the old auth_e::execute_edit_action calls with the new canvas library
/// NEW: Unified canvas action handler for any CanvasState with character fallback
/// Complete canvas action handler with fallbacks for common keys
/// Debug version to see what's happening
/// FIXED: Unified canvas action handler with proper priority order for edit mode
async fn handle_canvas_state_edit<S: CanvasState>(
key: KeyEvent,
config: &Config,
state: &mut S,
ideal_cursor_column: &mut usize,
) -> Result<String> {
println!("DEBUG: Key pressed: {:?}", key); // DEBUG
// Try direct key mapping first (same pattern as FormState)
// println!("DEBUG: Key pressed: {:?}", key); // DEBUG
// PRIORITY 1: Character insertion in edit mode comes FIRST
if let KeyCode::Char(c) = key.code {
// Only insert if no modifiers or just shift (for uppercase)
if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT {
// println!("DEBUG: Using character insertion priority for: {}", c); // DEBUG
let canvas_action = CanvasAction::InsertChar(c);
match ActionDispatcher::dispatch(canvas_action, state, ideal_cursor_column).await {
Ok(ActionResult::Success(msg)) => {
return Ok(msg.unwrap_or_default());
}
Ok(ActionResult::HandledByFeature(msg)) => {
return Ok(msg);
}
Ok(ActionResult::Error(msg)) => {
return Ok(format!("Error: {}", msg));
}
Ok(ActionResult::RequiresContext(msg)) => {
return Ok(format!("Context needed: {}", msg));
}
Err(e) => {
// println!("DEBUG: Character insertion failed: {:?}, trying config", e);
// Fall through to try config mappings
}
}
}
}
// PRIORITY 2: Check canvas config for special keys/combinations
let canvas_config = canvas::config::CanvasConfig::load();
if let Some(action_name) = canvas_config.get_edit_action(key.code, key.modifiers) {
println!("DEBUG: Canvas config mapped to: {}", action_name); // DEBUG
// println!("DEBUG: Canvas config mapped to: {}", action_name); // DEBUG
let canvas_action = CanvasAction::from_string(action_name);
match ActionDispatcher::dispatch(canvas_action, state, ideal_cursor_column).await {
@@ -176,62 +199,43 @@ async fn handle_canvas_state_edit<S: CanvasState>(
return Ok(format!("Context needed: {}", msg));
}
Err(_) => {
println!("DEBUG: Canvas action failed, trying client config"); // DEBUG
// println!("DEBUG: Canvas action failed, trying client config"); // DEBUG
}
}
} else {
println!("DEBUG: No canvas config mapping found"); // DEBUG
// println!("DEBUG: No canvas config mapping found"); // DEBUG
}
// Try config-mapped action (same pattern as FormState)
if let Some(action_str) = config.get_edit_action_for_key(key.code, key.modifiers) {
println!("DEBUG: Client config mapped to: {}", action_str); // DEBUG
let canvas_action = CanvasAction::from_string(&action_str);
match ActionDispatcher::dispatch(canvas_action, state, ideal_cursor_column).await {
Ok(ActionResult::Success(msg)) => {
return Ok(msg.unwrap_or_default());
}
Ok(ActionResult::HandledByFeature(msg)) => {
return Ok(msg);
}
Ok(ActionResult::Error(msg)) => {
return Ok(format!("Error: {}", msg));
}
Ok(ActionResult::RequiresContext(msg)) => {
return Ok(format!("Context needed: {}", msg));
}
Err(e) => {
return Ok(format!("Action failed: {}", e));
// PRIORITY 3: Check client config ONLY for non-character keys or modified keys
if !matches!(key.code, KeyCode::Char(_)) || !key.modifiers.is_empty() {
if let Some(action_str) = config.get_edit_action_for_key(key.code, key.modifiers) {
// println!("DEBUG: Client config mapped to: {} (for non-char key)", action_str); // DEBUG
let canvas_action = CanvasAction::from_string(&action_str);
match ActionDispatcher::dispatch(canvas_action, state, ideal_cursor_column).await {
Ok(ActionResult::Success(msg)) => {
return Ok(msg.unwrap_or_default());
}
Ok(ActionResult::HandledByFeature(msg)) => {
return Ok(msg);
}
Ok(ActionResult::Error(msg)) => {
return Ok(format!("Error: {}", msg));
}
Ok(ActionResult::RequiresContext(msg)) => {
return Ok(format!("Context needed: {}", msg));
}
Err(e) => {
return Ok(format!("Action failed: {}", e));
}
}
} else {
// println!("DEBUG: No client config mapping found for non-char key"); // DEBUG
}
} else {
println!("DEBUG: No client config mapping found"); // DEBUG
// println!("DEBUG: Skipping client config for character key in edit mode"); // DEBUG
}
// Character insertion fallback
if let KeyCode::Char(c) = key.code {
println!("DEBUG: Using character fallback for: {}", c); // DEBUG
let canvas_action = CanvasAction::InsertChar(c);
match ActionDispatcher::dispatch(canvas_action, state, ideal_cursor_column).await {
Ok(ActionResult::Success(msg)) => {
return Ok(msg.unwrap_or_default());
}
Ok(ActionResult::HandledByFeature(msg)) => {
return Ok(msg);
}
Ok(ActionResult::Error(msg)) => {
return Ok(format!("Error: {}", msg));
}
Ok(ActionResult::RequiresContext(msg)) => {
return Ok(format!("Context needed: {}", msg));
}
Err(e) => {
return Ok(format!("Character insertion failed: {}", e));
}
}
}
println!("DEBUG: No action taken for key: {:?}", key); // DEBUG
// println!("DEBUG: No action taken for key: {:?}", key); // DEBUG
Ok(String::new())
}

View File

@@ -45,6 +45,7 @@ use crate::ui::handlers::rat_state::UiStateHandler;
use anyhow::Result;
use common::proto::komp_ac::search::search_response::Hit;
use crossterm::cursor::SetCursorStyle;
use crossterm::event::KeyModifiers;
use crossterm::event::{Event, KeyCode, KeyEvent};
use tokio::sync::mpsc;
use tokio::sync::mpsc::unbounded_channel;
@@ -776,7 +777,6 @@ impl EventHandler {
if app_state.ui.show_form {
if let Ok(Some(canvas_message)) = self.handle_form_canvas_action(
key_event,
config,
form_state,
false,
).await {
@@ -866,7 +866,6 @@ impl EventHandler {
if app_state.ui.show_form {
if let Ok(Some(canvas_message)) = self.handle_form_canvas_action(
key_event,
config,
form_state,
true,
).await {
@@ -1102,18 +1101,39 @@ impl EventHandler {
async fn handle_form_canvas_action(
&mut self,
key_event: KeyEvent,
_config: &Config,
form_state: &mut FormState,
is_edit_mode: bool,
) -> Result<Option<String>> {
let canvas_config = canvas::config::CanvasConfig::load();
// Get action from config - handles all modes (edit/read-only/suggestions)
// PRIORITY 1: Handle character insertion in edit mode FIRST
if is_edit_mode {
if let KeyCode::Char(c) = key_event.code {
// Only insert if it's not a special modifier combination
if key_event.modifiers.is_empty() || key_event.modifiers == KeyModifiers::SHIFT {
let canvas_action = CanvasAction::InsertChar(c);
match ActionDispatcher::dispatch(
canvas_action,
form_state,
&mut self.ideal_cursor_column,
).await {
Ok(result) => {
return Ok(Some(result.message().unwrap_or("").to_string()));
}
Err(_) => {
return Ok(Some("Character insertion failed".to_string()));
}
}
}
}
}
// PRIORITY 2: Handle config-mapped actions for non-character keys
let action_str = canvas_config.get_action_for_key(
key_event.code,
key_event.modifiers,
is_edit_mode,
form_state.autocomplete_active
form_state.autocomplete_active,
);
if let Some(action_str) = action_str {
@@ -1138,25 +1158,6 @@ impl EventHandler {
}
}
// Handle character insertion for edit mode (not in config)
if is_edit_mode {
if let KeyCode::Char(c) = key_event.code {
let canvas_action = CanvasAction::InsertChar(c);
match ActionDispatcher::dispatch(
canvas_action,
form_state,
&mut self.ideal_cursor_column,
).await {
Ok(result) => {
return Ok(Some(result.message().unwrap_or("").to_string()));
}
Err(_) => {
return Ok(Some("Character insertion failed".to_string()));
}
}
}
}
// No action found
Ok(None)
}