automatic cursor style handled by the library
This commit is contained in:
@@ -12,7 +12,7 @@ categories.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
ratatui = { workspace = true, optional = true }
|
ratatui = { workspace = true, optional = true }
|
||||||
crossterm = { workspace = true }
|
crossterm = { workspace = true, optional = true }
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
tokio = { workspace = true, optional = true }
|
tokio = { workspace = true, optional = true }
|
||||||
toml = { workspace = true }
|
toml = { workspace = true }
|
||||||
@@ -31,6 +31,7 @@ tokio-test = "0.4.4"
|
|||||||
default = []
|
default = []
|
||||||
gui = ["ratatui"]
|
gui = ["ratatui"]
|
||||||
autocomplete = ["tokio"]
|
autocomplete = ["tokio"]
|
||||||
|
cursor-style = ["crossterm"]
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "autocomplete"
|
name = "autocomplete"
|
||||||
|
|||||||
@@ -1,388 +0,0 @@
|
|||||||
// examples/canvas_gui_demo.rs
|
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
CanvasAction, execute,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
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,
|
|
||||||
debug_message: "Ready - Use hjkl to move, w for next word, i to edit".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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Simple key mapping - users have full control!
|
|
||||||
async fn handle_key_press(key: KeyCode, modifiers: KeyModifiers, state: &mut DemoFormState) -> bool {
|
|
||||||
let is_edit_mode = state.mode == AppMode::Edit;
|
|
||||||
|
|
||||||
// Handle quit first
|
|
||||||
if (key == KeyCode::Char('q') && modifiers.contains(KeyModifiers::CONTROL)) ||
|
|
||||||
(key == KeyCode::Char('c') && modifiers.contains(KeyModifiers::CONTROL)) ||
|
|
||||||
key == KeyCode::F(10) {
|
|
||||||
return false; // Signal to quit
|
|
||||||
}
|
|
||||||
|
|
||||||
// Users directly map keys to actions - no configuration needed!
|
|
||||||
let action = match (state.mode, key, modifiers) {
|
|
||||||
// === READ-ONLY MODE KEYS ===
|
|
||||||
(AppMode::ReadOnly, KeyCode::Char('h'), _) => Some(CanvasAction::MoveLeft),
|
|
||||||
(AppMode::ReadOnly, KeyCode::Char('j'), _) => Some(CanvasAction::MoveDown),
|
|
||||||
(AppMode::ReadOnly, KeyCode::Char('k'), _) => Some(CanvasAction::MoveUp),
|
|
||||||
(AppMode::ReadOnly, KeyCode::Char('l'), _) => Some(CanvasAction::MoveRight),
|
|
||||||
(AppMode::ReadOnly, KeyCode::Char('w'), _) => Some(CanvasAction::MoveWordNext),
|
|
||||||
(AppMode::ReadOnly, KeyCode::Char('b'), _) => Some(CanvasAction::MoveWordPrev),
|
|
||||||
(AppMode::ReadOnly, KeyCode::Char('e'), _) => Some(CanvasAction::MoveWordEnd),
|
|
||||||
(AppMode::ReadOnly, KeyCode::Char('0'), _) => Some(CanvasAction::MoveLineStart),
|
|
||||||
(AppMode::ReadOnly, KeyCode::Char('$'), _) => Some(CanvasAction::MoveLineEnd),
|
|
||||||
(AppMode::ReadOnly, KeyCode::Tab, _) => Some(CanvasAction::NextField),
|
|
||||||
(AppMode::ReadOnly, KeyCode::BackTab, _) => Some(CanvasAction::PrevField),
|
|
||||||
|
|
||||||
// === EDIT MODE KEYS ===
|
|
||||||
(AppMode::Edit, KeyCode::Left, _) => Some(CanvasAction::MoveLeft),
|
|
||||||
(AppMode::Edit, KeyCode::Right, _) => Some(CanvasAction::MoveRight),
|
|
||||||
(AppMode::Edit, KeyCode::Up, _) => Some(CanvasAction::MoveUp),
|
|
||||||
(AppMode::Edit, KeyCode::Down, _) => Some(CanvasAction::MoveDown),
|
|
||||||
(AppMode::Edit, KeyCode::Home, _) => Some(CanvasAction::MoveLineStart),
|
|
||||||
(AppMode::Edit, KeyCode::End, _) => Some(CanvasAction::MoveLineEnd),
|
|
||||||
(AppMode::Edit, KeyCode::Backspace, _) => Some(CanvasAction::DeleteBackward),
|
|
||||||
(AppMode::Edit, KeyCode::Delete, _) => Some(CanvasAction::DeleteForward),
|
|
||||||
(AppMode::Edit, KeyCode::Tab, _) => Some(CanvasAction::NextField),
|
|
||||||
(AppMode::Edit, KeyCode::BackTab, _) => Some(CanvasAction::PrevField),
|
|
||||||
|
|
||||||
// Vim-style movement in edit mode (optional)
|
|
||||||
(AppMode::Edit, KeyCode::Char('h'), m) if m.contains(KeyModifiers::CONTROL) => Some(CanvasAction::MoveLeft),
|
|
||||||
(AppMode::Edit, KeyCode::Char('l'), m) if m.contains(KeyModifiers::CONTROL) => Some(CanvasAction::MoveRight),
|
|
||||||
|
|
||||||
// Word movement with Ctrl in edit mode
|
|
||||||
(AppMode::Edit, KeyCode::Left, m) if m.contains(KeyModifiers::CONTROL) => Some(CanvasAction::MoveWordPrev),
|
|
||||||
(AppMode::Edit, KeyCode::Right, m) if m.contains(KeyModifiers::CONTROL) => Some(CanvasAction::MoveWordNext),
|
|
||||||
|
|
||||||
// === MODE TRANSITIONS ===
|
|
||||||
(AppMode::ReadOnly, KeyCode::Char('i'), _) => Some(CanvasAction::Custom("enter_edit_mode".to_string())),
|
|
||||||
(AppMode::ReadOnly, KeyCode::Char('a'), _) => {
|
|
||||||
// 'a' moves to end of line then enters edit mode
|
|
||||||
if let Ok(_) = execute(CanvasAction::MoveLineEnd, state).await {
|
|
||||||
Some(CanvasAction::Custom("enter_edit_mode".to_string()))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(AppMode::ReadOnly, KeyCode::Char('v'), _) => Some(CanvasAction::Custom("enter_highlight_mode".to_string())),
|
|
||||||
(_, KeyCode::Esc, _) => Some(CanvasAction::Custom("enter_readonly_mode".to_string())),
|
|
||||||
|
|
||||||
// === CHARACTER INPUT IN EDIT MODE ===
|
|
||||||
(AppMode::Edit, KeyCode::Char(c), m) if !m.contains(KeyModifiers::CONTROL) && !m.contains(KeyModifiers::ALT) => {
|
|
||||||
Some(CanvasAction::InsertChar(c))
|
|
||||||
},
|
|
||||||
|
|
||||||
// === ARROW KEYS IN READ-ONLY MODE ===
|
|
||||||
(AppMode::ReadOnly, KeyCode::Left, _) => Some(CanvasAction::MoveLeft),
|
|
||||||
(AppMode::ReadOnly, KeyCode::Right, _) => Some(CanvasAction::MoveRight),
|
|
||||||
(AppMode::ReadOnly, KeyCode::Up, _) => Some(CanvasAction::MoveUp),
|
|
||||||
(AppMode::ReadOnly, KeyCode::Down, _) => Some(CanvasAction::MoveDown),
|
|
||||||
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Execute the action if we found one
|
|
||||||
if let Some(action) = action {
|
|
||||||
match execute(action.clone(), state).await {
|
|
||||||
Ok(result) => {
|
|
||||||
if result.is_success() {
|
|
||||||
// Mark as changed for editing actions
|
|
||||||
if is_edit_mode {
|
|
||||||
match action {
|
|
||||||
CanvasAction::InsertChar(_) | CanvasAction::DeleteBackward | CanvasAction::DeleteForward => {
|
|
||||||
state.set_has_unsaved_changes(true);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(msg) = result.message() {
|
|
||||||
state.debug_message = msg.to_string();
|
|
||||||
} else {
|
|
||||||
state.debug_message = format!("Executed: {}", action.description());
|
|
||||||
}
|
|
||||||
} else if let Some(msg) = result.message() {
|
|
||||||
state.debug_message = format!("Error: {}", msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
state.debug_message = format!("Error executing action: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
state.debug_message = format!("Unhandled key: {:?} (mode: {:?})", key, state.mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
true // Continue running
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut state: DemoFormState) -> io::Result<()> {
|
|
||||||
let theme = DemoTheme;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
terminal.draw(|f| ui(f, &state, &theme))?;
|
|
||||||
|
|
||||||
if let Event::Key(key) = event::read()? {
|
|
||||||
let should_continue = handle_key_press(key.code, key.modifiers, &mut state).await;
|
|
||||||
if !should_continue {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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: {} | Actions: {}",
|
|
||||||
state.current_field + 1,
|
|
||||||
state.fields.len(),
|
|
||||||
state.cursor_pos,
|
|
||||||
CanvasAction::movement_actions().len() + CanvasAction::editing_actions().len());
|
|
||||||
|
|
||||||
let help_text = match state.mode {
|
|
||||||
AppMode::ReadOnly => "hjkl/arrows: Move | Tab/Shift+Tab: Fields | w/b/e: Words | 0/$: Line | i/a: Edit | v: Visual | F10: Quit",
|
|
||||||
AppMode::Edit => "Type to edit | Arrows/Ctrl+arrows: Move | Tab: 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>> {
|
|
||||||
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).await;
|
|
||||||
|
|
||||||
disable_raw_mode()?;
|
|
||||||
execute!(
|
|
||||||
terminal.backend_mut(),
|
|
||||||
LeaveAlternateScreen,
|
|
||||||
DisableMouseCapture
|
|
||||||
)?;
|
|
||||||
terminal.show_cursor()?;
|
|
||||||
|
|
||||||
if let Err(err) = res {
|
|
||||||
println!("{:?}", err);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
|
cursor::SetCursorStyle,
|
||||||
event::{
|
event::{
|
||||||
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers,
|
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers,
|
||||||
},
|
},
|
||||||
@@ -28,12 +29,26 @@ use canvas::{
|
|||||||
DataProvider, FormEditor,
|
DataProvider, FormEditor,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Update cursor style based on current AppMode
|
||||||
|
fn update_cursor_for_mode(mode: AppMode) -> io::Result<()> {
|
||||||
|
let style = match mode {
|
||||||
|
AppMode::Edit => SetCursorStyle::SteadyBar, // Thin line for insert mode
|
||||||
|
AppMode::ReadOnly => SetCursorStyle::SteadyBlock, // Block for normal mode
|
||||||
|
AppMode::Highlight => SetCursorStyle::BlinkingBlock, // Blinking block for visual mode
|
||||||
|
AppMode::General => SetCursorStyle::SteadyBlock, // Block for general mode
|
||||||
|
AppMode::Command => SetCursorStyle::SteadyUnderScore, // Underscore for command mode
|
||||||
|
};
|
||||||
|
|
||||||
|
execute!(io::stdout(), style)
|
||||||
|
}
|
||||||
|
|
||||||
// Enhanced FormEditor that adds visual mode and status tracking
|
// Enhanced FormEditor that adds visual mode and status tracking
|
||||||
struct EnhancedFormEditor<D: DataProvider> {
|
struct EnhancedFormEditor<D: DataProvider> {
|
||||||
editor: FormEditor<D>,
|
editor: FormEditor<D>,
|
||||||
highlight_state: HighlightState,
|
highlight_state: HighlightState,
|
||||||
has_unsaved_changes: bool,
|
has_unsaved_changes: bool,
|
||||||
debug_message: String,
|
debug_message: String,
|
||||||
|
command_buffer: String, // For multi-key vim commands like "gg"
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<D: DataProvider> EnhancedFormEditor<D> {
|
impl<D: DataProvider> EnhancedFormEditor<D> {
|
||||||
@@ -43,11 +58,30 @@ impl<D: DataProvider> EnhancedFormEditor<D> {
|
|||||||
highlight_state: HighlightState::Off,
|
highlight_state: HighlightState::Off,
|
||||||
has_unsaved_changes: false,
|
has_unsaved_changes: false,
|
||||||
debug_message: "Full Canvas Demo - All features enabled".to_string(),
|
debug_message: "Full Canvas Demo - All features enabled".to_string(),
|
||||||
|
command_buffer: String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === COMMAND BUFFER HANDLING ===
|
||||||
|
|
||||||
|
fn clear_command_buffer(&mut self) {
|
||||||
|
self.command_buffer.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_to_command_buffer(&mut self, ch: char) {
|
||||||
|
self.command_buffer.push(ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_command_buffer(&self) -> &str {
|
||||||
|
&self.command_buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_pending_command(&self) -> bool {
|
||||||
|
!self.command_buffer.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
// === VISUAL/HIGHLIGHT MODE SUPPORT ===
|
// === VISUAL/HIGHLIGHT MODE SUPPORT ===
|
||||||
|
|
||||||
fn enter_visual_mode(&mut self) {
|
fn enter_visual_mode(&mut self) {
|
||||||
if ModeManager::can_enter_highlight_mode(self.editor.mode()) {
|
if ModeManager::can_enter_highlight_mode(self.editor.mode()) {
|
||||||
self.editor.set_mode(AppMode::Highlight);
|
self.editor.set_mode(AppMode::Highlight);
|
||||||
@@ -100,7 +134,7 @@ impl<D: DataProvider> EnhancedFormEditor<D> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// === ENHANCED MOVEMENT WITH VISUAL UPDATES ===
|
// === ENHANCED MOVEMENT WITH VISUAL UPDATES ===
|
||||||
|
|
||||||
fn move_left(&mut self) {
|
fn move_left(&mut self) {
|
||||||
self.editor.move_left();
|
self.editor.move_left();
|
||||||
self.update_visual_selection();
|
self.update_visual_selection();
|
||||||
@@ -172,7 +206,7 @@ impl<D: DataProvider> EnhancedFormEditor<D> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// === DELETE OPERATIONS ===
|
// === DELETE OPERATIONS ===
|
||||||
|
|
||||||
fn delete_backward(&mut self) -> anyhow::Result<()> {
|
fn delete_backward(&mut self) -> anyhow::Result<()> {
|
||||||
let result = self.editor.delete_backward();
|
let result = self.editor.delete_backward();
|
||||||
if result.is_ok() {
|
if result.is_ok() {
|
||||||
@@ -192,7 +226,7 @@ impl<D: DataProvider> EnhancedFormEditor<D> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// === MODE TRANSITIONS ===
|
// === MODE TRANSITIONS ===
|
||||||
|
|
||||||
fn enter_edit_mode(&mut self) {
|
fn enter_edit_mode(&mut self) {
|
||||||
self.editor.enter_edit_mode();
|
self.editor.enter_edit_mode();
|
||||||
self.debug_message = "-- INSERT --".to_string();
|
self.debug_message = "-- INSERT --".to_string();
|
||||||
@@ -217,23 +251,23 @@ impl<D: DataProvider> EnhancedFormEditor<D> {
|
|||||||
fn current_field(&self) -> usize {
|
fn current_field(&self) -> usize {
|
||||||
self.editor.current_field()
|
self.editor.current_field()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cursor_position(&self) -> usize {
|
fn cursor_position(&self) -> usize {
|
||||||
self.editor.cursor_position()
|
self.editor.cursor_position()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mode(&self) -> AppMode {
|
fn mode(&self) -> AppMode {
|
||||||
self.editor.mode()
|
self.editor.mode()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_text(&self) -> &str {
|
fn current_text(&self) -> &str {
|
||||||
self.editor.current_text()
|
self.editor.current_text()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn data_provider(&self) -> &D {
|
fn data_provider(&self) -> &D {
|
||||||
self.editor.data_provider()
|
self.editor.data_provider()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ui_state(&self) -> &canvas::EditorState {
|
fn ui_state(&self) -> &canvas::EditorState {
|
||||||
self.editor.ui_state()
|
self.editor.ui_state()
|
||||||
}
|
}
|
||||||
@@ -246,7 +280,7 @@ impl<D: DataProvider> EnhancedFormEditor<D> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// === STATUS AND DEBUG ===
|
// === STATUS AND DEBUG ===
|
||||||
|
|
||||||
fn set_debug_message(&mut self, msg: String) {
|
fn set_debug_message(&mut self, msg: String) {
|
||||||
self.debug_message = msg;
|
self.debug_message = msg;
|
||||||
}
|
}
|
||||||
@@ -298,23 +332,23 @@ impl DataProvider for FullDemoData {
|
|||||||
fn field_count(&self) -> usize {
|
fn field_count(&self) -> usize {
|
||||||
self.fields.len()
|
self.fields.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn field_name(&self, index: usize) -> &str {
|
fn field_name(&self, index: usize) -> &str {
|
||||||
&self.fields[index].0
|
&self.fields[index].0
|
||||||
}
|
}
|
||||||
|
|
||||||
fn field_value(&self, index: usize) -> &str {
|
fn field_value(&self, index: usize) -> &str {
|
||||||
&self.fields[index].1
|
&self.fields[index].1
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_field_value(&mut self, index: usize, value: String) {
|
fn set_field_value(&mut self, index: usize, value: String) {
|
||||||
self.fields[index].1 = value;
|
self.fields[index].1 = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn supports_autocomplete(&self, _field_index: usize) -> bool {
|
fn supports_autocomplete(&self, _field_index: usize) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn display_value(&self, _index: usize) -> Option<&str> {
|
fn display_value(&self, _index: usize) -> Option<&str> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -326,7 +360,7 @@ fn handle_key_press(
|
|||||||
modifiers: KeyModifiers,
|
modifiers: KeyModifiers,
|
||||||
editor: &mut EnhancedFormEditor<FullDemoData>,
|
editor: &mut EnhancedFormEditor<FullDemoData>,
|
||||||
) -> anyhow::Result<bool> {
|
) -> anyhow::Result<bool> {
|
||||||
let mode = editor.mode();
|
let old_mode = editor.mode(); // Store mode before processing
|
||||||
|
|
||||||
// Quit handling
|
// Quit handling
|
||||||
if (key == KeyCode::Char('q') && modifiers.contains(KeyModifiers::CONTROL))
|
if (key == KeyCode::Char('q') && modifiers.contains(KeyModifiers::CONTROL))
|
||||||
@@ -336,34 +370,41 @@ fn handle_key_press(
|
|||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
match (mode, key, modifiers) {
|
match (old_mode, key, modifiers) {
|
||||||
// === MODE TRANSITIONS ===
|
// === MODE TRANSITIONS ===
|
||||||
(AppMode::ReadOnly, KeyCode::Char('i'), _) => {
|
(AppMode::ReadOnly, KeyCode::Char('i'), _) => {
|
||||||
editor.enter_edit_mode();
|
editor.enter_edit_mode();
|
||||||
|
editor.clear_command_buffer();
|
||||||
}
|
}
|
||||||
(AppMode::ReadOnly, KeyCode::Char('a'), _) => {
|
(AppMode::ReadOnly, KeyCode::Char('a'), _) => {
|
||||||
editor.move_right(); // Move after current character
|
editor.move_right(); // Move after current character
|
||||||
editor.enter_edit_mode();
|
editor.enter_edit_mode();
|
||||||
editor.set_debug_message("-- INSERT -- (append)".to_string());
|
editor.set_debug_message("-- INSERT -- (append)".to_string());
|
||||||
|
editor.clear_command_buffer();
|
||||||
}
|
}
|
||||||
(AppMode::ReadOnly, KeyCode::Char('A'), _) => {
|
(AppMode::ReadOnly, KeyCode::Char('A'), _) => {
|
||||||
editor.move_line_end();
|
editor.move_line_end();
|
||||||
editor.enter_edit_mode();
|
editor.enter_edit_mode();
|
||||||
editor.set_debug_message("-- INSERT -- (end of line)".to_string());
|
editor.set_debug_message("-- INSERT -- (end of line)".to_string());
|
||||||
|
editor.clear_command_buffer();
|
||||||
}
|
}
|
||||||
(AppMode::ReadOnly, KeyCode::Char('o'), _) => {
|
(AppMode::ReadOnly, KeyCode::Char('o'), _) => {
|
||||||
editor.move_line_end();
|
editor.move_line_end();
|
||||||
editor.enter_edit_mode();
|
editor.enter_edit_mode();
|
||||||
editor.set_debug_message("-- INSERT -- (open line)".to_string());
|
editor.set_debug_message("-- INSERT -- (open line)".to_string());
|
||||||
|
editor.clear_command_buffer();
|
||||||
}
|
}
|
||||||
(AppMode::ReadOnly, KeyCode::Char('v'), _) => {
|
(AppMode::ReadOnly, KeyCode::Char('v'), _) => {
|
||||||
editor.enter_visual_mode();
|
editor.enter_visual_mode();
|
||||||
|
editor.clear_command_buffer();
|
||||||
}
|
}
|
||||||
(AppMode::ReadOnly, KeyCode::Char('V'), _) => {
|
(AppMode::ReadOnly, KeyCode::Char('V'), _) => {
|
||||||
editor.enter_visual_line_mode();
|
editor.enter_visual_line_mode();
|
||||||
|
editor.clear_command_buffer();
|
||||||
}
|
}
|
||||||
(_, KeyCode::Esc, _) => {
|
(_, KeyCode::Esc, _) => {
|
||||||
editor.exit_edit_mode();
|
editor.exit_edit_mode();
|
||||||
|
editor.clear_command_buffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
// === MOVEMENT: VIM-STYLE NAVIGATION ===
|
// === MOVEMENT: VIM-STYLE NAVIGATION ===
|
||||||
@@ -373,39 +414,47 @@ fn handle_key_press(
|
|||||||
| (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Left, _) => {
|
| (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Left, _) => {
|
||||||
editor.move_left();
|
editor.move_left();
|
||||||
editor.set_debug_message("← left".to_string());
|
editor.set_debug_message("← left".to_string());
|
||||||
|
editor.clear_command_buffer();
|
||||||
}
|
}
|
||||||
(AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('l'), _)
|
(AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('l'), _)
|
||||||
| (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Right, _) => {
|
| (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Right, _) => {
|
||||||
editor.move_right();
|
editor.move_right();
|
||||||
editor.set_debug_message("→ right".to_string());
|
editor.set_debug_message("→ right".to_string());
|
||||||
|
editor.clear_command_buffer();
|
||||||
}
|
}
|
||||||
(AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('j'), _)
|
(AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('j'), _)
|
||||||
| (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Down, _) => {
|
| (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Down, _) => {
|
||||||
editor.move_down();
|
editor.move_down();
|
||||||
editor.set_debug_message("↓ next field".to_string());
|
editor.set_debug_message("↓ next field".to_string());
|
||||||
|
editor.clear_command_buffer();
|
||||||
}
|
}
|
||||||
(AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('k'), _)
|
(AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('k'), _)
|
||||||
| (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Up, _) => {
|
| (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Up, _) => {
|
||||||
editor.move_up();
|
editor.move_up();
|
||||||
editor.set_debug_message("↑ previous field".to_string());
|
editor.set_debug_message("↑ previous field".to_string());
|
||||||
|
editor.clear_command_buffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Word movement - Full vim word navigation
|
// Word movement - Full vim word navigation
|
||||||
(AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('w'), _) => {
|
(AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('w'), _) => {
|
||||||
editor.move_word_next();
|
editor.move_word_next();
|
||||||
editor.set_debug_message("w: next word start".to_string());
|
editor.set_debug_message("w: next word start".to_string());
|
||||||
|
editor.clear_command_buffer();
|
||||||
}
|
}
|
||||||
(AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('b'), _) => {
|
(AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('b'), _) => {
|
||||||
editor.move_word_prev();
|
editor.move_word_prev();
|
||||||
editor.set_debug_message("b: previous word start".to_string());
|
editor.set_debug_message("b: previous word start".to_string());
|
||||||
|
editor.clear_command_buffer();
|
||||||
}
|
}
|
||||||
(AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('e'), _) => {
|
(AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('e'), _) => {
|
||||||
editor.move_word_end();
|
editor.move_word_end();
|
||||||
editor.set_debug_message("e: word end".to_string());
|
editor.set_debug_message("e: word end".to_string());
|
||||||
|
editor.clear_command_buffer();
|
||||||
}
|
}
|
||||||
(AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('W'), _) => {
|
(AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('W'), _) => {
|
||||||
editor.move_word_end_prev();
|
editor.move_word_end_prev();
|
||||||
editor.set_debug_message("W: previous word end".to_string());
|
editor.set_debug_message("W: previous word end".to_string());
|
||||||
|
editor.clear_command_buffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Line movement
|
// Line movement
|
||||||
@@ -422,15 +471,33 @@ fn handle_key_press(
|
|||||||
|
|
||||||
// Field/document movement
|
// Field/document movement
|
||||||
(AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('g'), _) => {
|
(AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('g'), _) => {
|
||||||
editor.move_first_line();
|
if editor.get_command_buffer() == "g" {
|
||||||
editor.set_debug_message("gg: first field".to_string());
|
// Second 'g' - execute "gg" command
|
||||||
|
editor.move_first_line();
|
||||||
|
editor.set_debug_message("gg: first field".to_string());
|
||||||
|
editor.clear_command_buffer();
|
||||||
|
} else {
|
||||||
|
// First 'g' - start command buffer
|
||||||
|
editor.clear_command_buffer();
|
||||||
|
editor.add_to_command_buffer('g');
|
||||||
|
editor.set_debug_message("g".to_string());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
(AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('G'), _) => {
|
(AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('G'), _) => {
|
||||||
editor.move_last_line();
|
editor.move_last_line();
|
||||||
editor.set_debug_message("G: last field".to_string());
|
editor.set_debug_message("G: last field".to_string());
|
||||||
|
editor.clear_command_buffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
// === EDIT MODE MOVEMENT ===
|
// === EDIT MODE MOVEMENT ===
|
||||||
|
(AppMode::Edit, KeyCode::Left, m) if m.contains(KeyModifiers::CONTROL) => {
|
||||||
|
editor.move_word_prev();
|
||||||
|
editor.set_debug_message("Ctrl+← word back".to_string());
|
||||||
|
}
|
||||||
|
(AppMode::Edit, KeyCode::Right, m) if m.contains(KeyModifiers::CONTROL) => {
|
||||||
|
editor.move_word_next();
|
||||||
|
editor.set_debug_message("Ctrl+→ word forward".to_string());
|
||||||
|
}
|
||||||
(AppMode::Edit, KeyCode::Left, _) => {
|
(AppMode::Edit, KeyCode::Left, _) => {
|
||||||
editor.move_left();
|
editor.move_left();
|
||||||
}
|
}
|
||||||
@@ -450,16 +517,6 @@ fn handle_key_press(
|
|||||||
editor.move_line_end();
|
editor.move_line_end();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Word movement in edit mode with Ctrl
|
|
||||||
(AppMode::Edit, KeyCode::Left, m) if m.contains(KeyModifiers::CONTROL) => {
|
|
||||||
editor.move_word_prev();
|
|
||||||
editor.set_debug_message("Ctrl+← word back".to_string());
|
|
||||||
}
|
|
||||||
(AppMode::Edit, KeyCode::Right, m) if m.contains(KeyModifiers::CONTROL) => {
|
|
||||||
editor.move_word_next();
|
|
||||||
editor.set_debug_message("Ctrl+→ word forward".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
// === DELETE OPERATIONS ===
|
// === DELETE OPERATIONS ===
|
||||||
(AppMode::Edit, KeyCode::Backspace, _) => {
|
(AppMode::Edit, KeyCode::Backspace, _) => {
|
||||||
editor.delete_backward()?;
|
editor.delete_backward()?;
|
||||||
@@ -496,7 +553,7 @@ fn handle_key_press(
|
|||||||
// === DEBUG/INFO COMMANDS ===
|
// === DEBUG/INFO COMMANDS ===
|
||||||
(AppMode::ReadOnly, KeyCode::Char('?'), _) => {
|
(AppMode::ReadOnly, KeyCode::Char('?'), _) => {
|
||||||
editor.set_debug_message(format!(
|
editor.set_debug_message(format!(
|
||||||
"Field {}/{}, Pos {}, Mode: {:?}",
|
"Field {}/{}, Pos {}, Mode: {:?}",
|
||||||
editor.current_field() + 1,
|
editor.current_field() + 1,
|
||||||
editor.data_provider().field_count(),
|
editor.data_provider().field_count(),
|
||||||
editor.cursor_position(),
|
editor.cursor_position(),
|
||||||
@@ -505,13 +562,25 @@ fn handle_key_press(
|
|||||||
}
|
}
|
||||||
|
|
||||||
_ => {
|
_ => {
|
||||||
editor.set_debug_message(format!(
|
// If we have a pending command and this key doesn't complete it, clear the buffer
|
||||||
"Unhandled: {:?} + {:?} in {:?} mode",
|
if editor.has_pending_command() {
|
||||||
key, modifiers, mode
|
editor.clear_command_buffer();
|
||||||
));
|
editor.set_debug_message("Invalid command sequence".to_string());
|
||||||
|
} else {
|
||||||
|
editor.set_debug_message(format!(
|
||||||
|
"Unhandled: {:?} + {:?} in {:?} mode",
|
||||||
|
key, modifiers, old_mode
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update cursor if mode changed
|
||||||
|
let new_mode = editor.mode();
|
||||||
|
if old_mode != new_mode {
|
||||||
|
update_cursor_for_mode(new_mode)?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -579,7 +648,9 @@ fn render_status_and_help(
|
|||||||
_ => "NORMAL",
|
_ => "NORMAL",
|
||||||
};
|
};
|
||||||
|
|
||||||
let status_text = if editor.has_unsaved_changes() {
|
let status_text = if editor.has_pending_command() {
|
||||||
|
format!("-- {} -- {} [{}]", mode_text, editor.debug_message(), editor.get_command_buffer())
|
||||||
|
} else if editor.has_unsaved_changes() {
|
||||||
format!("-- {} -- [Modified] {}", mode_text, editor.debug_message())
|
format!("-- {} -- [Modified] {}", mode_text, editor.debug_message())
|
||||||
} else {
|
} else {
|
||||||
format!("-- {} -- {}", mode_text, editor.debug_message())
|
format!("-- {} -- {}", mode_text, editor.debug_message())
|
||||||
@@ -593,7 +664,14 @@ fn render_status_and_help(
|
|||||||
// Help text
|
// Help text
|
||||||
let help_text = match editor.mode() {
|
let help_text = match editor.mode() {
|
||||||
AppMode::ReadOnly => {
|
AppMode::ReadOnly => {
|
||||||
"Normal: hjkl/arrows=move, w/b/e=words, 0/$=line, gg/G=first/last, i/a/A=insert, v/V=visual, x/X=delete, ?=info"
|
if editor.has_pending_command() {
|
||||||
|
match editor.get_command_buffer() {
|
||||||
|
"g" => "Press 'g' again for first field, or any other key to cancel",
|
||||||
|
_ => "Pending command... (Esc to cancel)"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"Normal: hjkl/arrows=move, w/b/e=words, 0/$=line, gg/G=first/last, i/a/A=insert, v/V=visual, x/X=delete, ?=info"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
AppMode::Edit => {
|
AppMode::Edit => {
|
||||||
"Insert: arrows=move, Ctrl+arrows=words, Backspace/Del=delete, Esc=normal, Tab/Shift+Tab=fields"
|
"Insert: arrows=move, Ctrl+arrows=words, Backspace/Del=delete, Esc=normal, Tab/Shift+Tab=fields"
|
||||||
@@ -621,9 +699,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let data = FullDemoData::new();
|
let data = FullDemoData::new();
|
||||||
let mut editor = EnhancedFormEditor::new(data);
|
let mut editor = EnhancedFormEditor::new(data);
|
||||||
editor.set_mode(AppMode::ReadOnly); // Start in normal mode
|
editor.set_mode(AppMode::ReadOnly); // Start in normal mode
|
||||||
|
|
||||||
|
// Set initial cursor style
|
||||||
|
update_cursor_for_mode(editor.mode())?;
|
||||||
|
|
||||||
let res = run_app(&mut terminal, editor);
|
let res = run_app(&mut terminal, editor);
|
||||||
|
|
||||||
|
// Reset cursor style on exit
|
||||||
|
execute!(io::stdout(), SetCursorStyle::DefaultUserShape)?;
|
||||||
|
|
||||||
disable_raw_mode()?;
|
disable_raw_mode()?;
|
||||||
execute!(
|
execute!(
|
||||||
terminal.backend_mut(),
|
terminal.backend_mut(),
|
||||||
|
|||||||
45
canvas/src/canvas/cursor.rs
Normal file
45
canvas/src/canvas/cursor.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// src/canvas/cursor.rs
|
||||||
|
//! Cursor style management for different canvas modes
|
||||||
|
|
||||||
|
#[cfg(feature = "cursor-style")]
|
||||||
|
use crossterm::{cursor::SetCursorStyle, execute};
|
||||||
|
#[cfg(feature = "cursor-style")]
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
use crate::canvas::modes::AppMode;
|
||||||
|
|
||||||
|
/// Manages cursor styles based on canvas modes
|
||||||
|
pub struct CursorManager;
|
||||||
|
|
||||||
|
impl CursorManager {
|
||||||
|
/// Update cursor style based on current mode
|
||||||
|
#[cfg(feature = "cursor-style")]
|
||||||
|
pub fn update_for_mode(mode: AppMode) -> io::Result<()> {
|
||||||
|
let style = match mode {
|
||||||
|
AppMode::Edit => SetCursorStyle::SteadyBar, // Thin line for insert
|
||||||
|
AppMode::ReadOnly => SetCursorStyle::SteadyBlock, // Block for normal
|
||||||
|
AppMode::Highlight => SetCursorStyle::BlinkingBlock, // Blinking for visual
|
||||||
|
AppMode::General => SetCursorStyle::SteadyBlock, // Block for general
|
||||||
|
AppMode::Command => SetCursorStyle::SteadyUnderScore, // Underscore for command
|
||||||
|
};
|
||||||
|
|
||||||
|
execute!(io::stdout(), style)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// No-op when cursor-style feature is disabled
|
||||||
|
#[cfg(not(feature = "cursor-style"))]
|
||||||
|
pub fn update_for_mode(_mode: AppMode) -> io::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset cursor to default on cleanup
|
||||||
|
#[cfg(feature = "cursor-style")]
|
||||||
|
pub fn reset() -> io::Result<()> {
|
||||||
|
execute!(io::stdout(), SetCursorStyle::DefaultUserShape)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "cursor-style"))]
|
||||||
|
pub fn reset() -> io::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,9 +6,14 @@ pub mod modes;
|
|||||||
|
|
||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
pub mod gui;
|
pub mod gui;
|
||||||
|
|
||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
pub mod theme;
|
pub mod theme;
|
||||||
|
|
||||||
|
#[cfg(feature = "cursor-style")]
|
||||||
|
pub mod cursor;
|
||||||
|
|
||||||
// Keep these exports for current functionality
|
// Keep these exports for current functionality
|
||||||
pub use modes::{AppMode, ModeManager, HighlightState};
|
pub use modes::{AppMode, ModeManager, HighlightState};
|
||||||
|
|
||||||
|
#[cfg(feature = "cursor-style")]
|
||||||
|
pub use cursor::CursorManager;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// src/modes/handlers/mode_manager.rs
|
// src/modes/handlers/mode_manager.rs
|
||||||
// canvas/src/modes/manager.rs
|
// canvas/src/modes/manager.rs
|
||||||
|
|
||||||
|
#[cfg(feature = "cursor-style")]
|
||||||
|
use crate::canvas::CursorManager;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum AppMode {
|
pub enum AppMode {
|
||||||
@@ -30,4 +32,39 @@ impl ModeManager {
|
|||||||
pub fn can_enter_highlight_mode(current_mode: AppMode) -> bool {
|
pub fn can_enter_highlight_mode(current_mode: AppMode) -> bool {
|
||||||
matches!(current_mode, AppMode::ReadOnly)
|
matches!(current_mode, AppMode::ReadOnly)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Transition to new mode with automatic cursor update (when cursor-style feature enabled)
|
||||||
|
pub fn transition_to_mode(current_mode: AppMode, new_mode: AppMode) -> std::io::Result<AppMode> {
|
||||||
|
if current_mode != new_mode {
|
||||||
|
#[cfg(feature = "cursor-style")]
|
||||||
|
{
|
||||||
|
let _ = CursorManager::update_for_mode(new_mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(new_mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enter highlight mode with cursor styling
|
||||||
|
pub fn enter_highlight_mode_with_cursor(current_mode: AppMode) -> std::io::Result<bool> {
|
||||||
|
if Self::can_enter_highlight_mode(current_mode) {
|
||||||
|
#[cfg(feature = "cursor-style")]
|
||||||
|
{
|
||||||
|
let _ = CursorManager::update_for_mode(AppMode::Highlight);
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Exit highlight mode with cursor styling
|
||||||
|
pub fn exit_highlight_mode_with_cursor() -> std::io::Result<AppMode> {
|
||||||
|
let new_mode = AppMode::ReadOnly;
|
||||||
|
#[cfg(feature = "cursor-style")]
|
||||||
|
{
|
||||||
|
let _ = CursorManager::update_for_mode(new_mode);
|
||||||
|
}
|
||||||
|
Ok(new_mode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
// src/editor.rs
|
// src/editor.rs
|
||||||
//! Main API for the canvas library - FormEditor with library-owned state
|
//! Main API for the canvas library - FormEditor with library-owned state
|
||||||
|
|
||||||
|
#[cfg(feature = "cursor-style")]
|
||||||
|
use crate::canvas::CursorManager;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use crate::canvas::state::EditorState;
|
use crate::canvas::state::EditorState;
|
||||||
use crate::data_provider::{DataProvider, AutocompleteProvider, SuggestionItem};
|
use crate::data_provider::{DataProvider, AutocompleteProvider, SuggestionItem};
|
||||||
@@ -145,12 +148,21 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
|
|
||||||
/// Change mode (for vim compatibility)
|
/// Change mode (for vim compatibility)
|
||||||
pub fn set_mode(&mut self, mode: AppMode) {
|
pub fn set_mode(&mut self, mode: AppMode) {
|
||||||
|
#[cfg(feature = "cursor-style")]
|
||||||
|
let old_mode = self.ui_state.current_mode;
|
||||||
|
|
||||||
self.ui_state.current_mode = mode;
|
self.ui_state.current_mode = mode;
|
||||||
|
|
||||||
// Clear autocomplete when changing modes
|
// Clear autocomplete when changing modes
|
||||||
if mode != AppMode::Edit {
|
if mode != AppMode::Edit {
|
||||||
self.ui_state.deactivate_autocomplete();
|
self.ui_state.deactivate_autocomplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update cursor style if mode changed and cursor-style feature is enabled
|
||||||
|
#[cfg(feature = "cursor-style")]
|
||||||
|
if old_mode != mode {
|
||||||
|
let _ = crate::canvas::CursorManager::update_for_mode(mode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
@@ -457,4 +469,75 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
|
|
||||||
self.ui_state.cursor_pos = safe_pos;
|
self.ui_state.cursor_pos = safe_pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Set the value of the current field
|
||||||
|
pub fn set_current_field_value(&mut self, value: String) {
|
||||||
|
let field_index = self.ui_state.current_field;
|
||||||
|
self.data_provider.set_field_value(field_index, value);
|
||||||
|
// Reset cursor to start of field
|
||||||
|
self.ui_state.cursor_pos = 0;
|
||||||
|
self.ui_state.ideal_cursor_column = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the value of a specific field by index
|
||||||
|
pub fn set_field_value(&mut self, field_index: usize, value: String) {
|
||||||
|
if field_index < self.data_provider.field_count() {
|
||||||
|
self.data_provider.set_field_value(field_index, value);
|
||||||
|
// If we're modifying the current field, reset cursor
|
||||||
|
if field_index == self.ui_state.current_field {
|
||||||
|
self.ui_state.cursor_pos = 0;
|
||||||
|
self.ui_state.ideal_cursor_column = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear the current field (set to empty string)
|
||||||
|
pub fn clear_current_field(&mut self) {
|
||||||
|
self.set_current_field_value(String::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get mutable access to data provider (for advanced operations)
|
||||||
|
pub fn data_provider_mut(&mut self) -> &mut D {
|
||||||
|
&mut self.data_provider
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set cursor to exact position (for vim-style movements like f, F, t, T)
|
||||||
|
pub fn set_cursor_position(&mut self, position: usize) {
|
||||||
|
let current_text = self.current_text();
|
||||||
|
let is_edit_mode = self.ui_state.current_mode == AppMode::Edit;
|
||||||
|
|
||||||
|
// Clamp to valid bounds for current mode
|
||||||
|
let max_pos = if is_edit_mode {
|
||||||
|
current_text.len() // Edit mode: can go past end
|
||||||
|
} else {
|
||||||
|
current_text.len().saturating_sub(1).max(0) // Read-only: stay within text
|
||||||
|
};
|
||||||
|
|
||||||
|
let clamped_pos = position.min(max_pos);
|
||||||
|
|
||||||
|
// Update cursor position directly
|
||||||
|
self.ui_state.cursor_pos = clamped_pos;
|
||||||
|
self.ui_state.ideal_cursor_column = clamped_pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cleanup cursor style (call this when shutting down)
|
||||||
|
pub fn cleanup_cursor(&self) -> std::io::Result<()> {
|
||||||
|
#[cfg(feature = "cursor-style")]
|
||||||
|
{
|
||||||
|
crate::canvas::CursorManager::reset()
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "cursor-style"))]
|
||||||
|
{
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add Drop implementation for automatic cleanup
|
||||||
|
impl<D: DataProvider> Drop for FormEditor<D> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// Reset cursor to default when FormEditor is dropped
|
||||||
|
let _ = self.cleanup_cursor();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ pub mod data_provider;
|
|||||||
#[cfg(feature = "autocomplete")]
|
#[cfg(feature = "autocomplete")]
|
||||||
pub mod autocomplete;
|
pub mod autocomplete;
|
||||||
|
|
||||||
|
#[cfg(feature = "cursor-style")]
|
||||||
|
pub use canvas::CursorManager;
|
||||||
|
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
// NEW API: Library-owned state pattern
|
// NEW API: Library-owned state pattern
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user