Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
baa4295059 | ||
|
|
6cbfac9d6e | ||
|
|
13d28f19ea | ||
|
|
8fa86965b8 | ||
|
|
72c38f613f |
@@ -3,7 +3,7 @@ use crate::config::colors::themes::Theme;
|
||||
use crate::state::app::highlight::HighlightState;
|
||||
use crate::state::app::state::AppState;
|
||||
use crate::state::pages::add_table::{AddTableFocus, AddTableState};
|
||||
use canvas::canvas::{render_canvas, CanvasState, HighlightState as CanvasHighlightState}; // Use canvas library
|
||||
use canvas::canvas::{render_canvas, CanvasState, HighlightState as CanvasHighlightState};
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Modifier, Style},
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// src/components/handlers.rs
|
||||
pub mod canvas;
|
||||
pub mod sidebar;
|
||||
pub mod buffer_list;
|
||||
|
||||
pub use canvas::*;
|
||||
pub use sidebar::*;
|
||||
pub use buffer_list::*;
|
||||
|
||||
@@ -1,255 +0,0 @@
|
||||
// src/components/handlers/canvas.rs
|
||||
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Modifier, Style},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
use crate::config::colors::themes::Theme;
|
||||
use crate::state::app::highlight::HighlightState;
|
||||
use crate::state::pages::canvas_state::CanvasState as LegacyCanvasState;
|
||||
use canvas::canvas::CanvasState as LibraryCanvasState;
|
||||
use std::cmp::{max, min};
|
||||
|
||||
/// Render canvas for legacy CanvasState (AddTableState, LoginState, RegisterState, AddLogicState)
|
||||
pub fn render_canvas(
|
||||
f: &mut Frame,
|
||||
area: Rect,
|
||||
form_state: &impl LegacyCanvasState,
|
||||
fields: &[&str],
|
||||
current_field_idx: &usize,
|
||||
inputs: &[&String],
|
||||
theme: &Theme,
|
||||
is_edit_mode: bool,
|
||||
highlight_state: &HighlightState,
|
||||
) -> Option<Rect> {
|
||||
render_canvas_impl(
|
||||
f,
|
||||
area,
|
||||
fields,
|
||||
current_field_idx,
|
||||
inputs,
|
||||
theme,
|
||||
is_edit_mode,
|
||||
highlight_state,
|
||||
form_state.current_cursor_pos(),
|
||||
form_state.has_unsaved_changes(),
|
||||
|i| form_state.get_display_value_for_field(i).to_string(),
|
||||
|i| form_state.has_display_override(i),
|
||||
)
|
||||
}
|
||||
|
||||
/// Render canvas for library CanvasState (FormState)
|
||||
pub fn render_canvas_library(
|
||||
f: &mut Frame,
|
||||
area: Rect,
|
||||
form_state: &impl LibraryCanvasState,
|
||||
fields: &[&str],
|
||||
current_field_idx: &usize,
|
||||
inputs: &[&String],
|
||||
theme: &Theme,
|
||||
is_edit_mode: bool,
|
||||
highlight_state: &HighlightState,
|
||||
) -> Option<Rect> {
|
||||
render_canvas_impl(
|
||||
f,
|
||||
area,
|
||||
fields,
|
||||
current_field_idx,
|
||||
inputs,
|
||||
theme,
|
||||
is_edit_mode,
|
||||
highlight_state,
|
||||
form_state.current_cursor_pos(),
|
||||
form_state.has_unsaved_changes(),
|
||||
|i| form_state.get_display_value_for_field(i).to_string(),
|
||||
|i| form_state.has_display_override(i),
|
||||
)
|
||||
}
|
||||
|
||||
/// Internal implementation shared by both render functions
|
||||
fn render_canvas_impl<F1, F2>(
|
||||
f: &mut Frame,
|
||||
area: Rect,
|
||||
fields: &[&str],
|
||||
current_field_idx: &usize,
|
||||
inputs: &[&String],
|
||||
theme: &Theme,
|
||||
is_edit_mode: bool,
|
||||
highlight_state: &HighlightState,
|
||||
current_cursor_pos: usize,
|
||||
has_unsaved_changes: bool,
|
||||
get_display_value: F1,
|
||||
has_display_override: F2,
|
||||
) -> Option<Rect>
|
||||
where
|
||||
F1: Fn(usize) -> String,
|
||||
F2: Fn(usize) -> bool,
|
||||
{
|
||||
let columns = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
|
||||
.split(area);
|
||||
|
||||
let border_style = if has_unsaved_changes {
|
||||
Style::default().fg(theme.warning)
|
||||
} else if is_edit_mode {
|
||||
Style::default().fg(theme.accent)
|
||||
} else {
|
||||
Style::default().fg(theme.secondary)
|
||||
};
|
||||
let input_container = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(border_style)
|
||||
.style(Style::default().bg(theme.bg));
|
||||
|
||||
let input_block = Rect {
|
||||
x: columns[1].x,
|
||||
y: columns[1].y,
|
||||
width: columns[1].width,
|
||||
height: fields.len() as u16 + 2,
|
||||
};
|
||||
|
||||
f.render_widget(&input_container, input_block);
|
||||
|
||||
let input_area = input_container.inner(input_block);
|
||||
let input_rows = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![Constraint::Length(1); fields.len()])
|
||||
.split(input_area);
|
||||
|
||||
let mut active_field_input_rect = None;
|
||||
|
||||
for (i, field) in fields.iter().enumerate() {
|
||||
let label = Paragraph::new(Line::from(Span::styled(
|
||||
format!("{}:", field),
|
||||
Style::default().fg(theme.fg),
|
||||
)));
|
||||
f.render_widget(
|
||||
label,
|
||||
Rect {
|
||||
x: columns[0].x,
|
||||
y: input_block.y + 1 + i as u16,
|
||||
width: columns[0].width,
|
||||
height: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
for (i, _input) in inputs.iter().enumerate() {
|
||||
let is_active = i == *current_field_idx;
|
||||
|
||||
// Use the provided closure to get display value
|
||||
let text = get_display_value(i);
|
||||
let text_len = text.chars().count();
|
||||
let line: Line;
|
||||
|
||||
match highlight_state {
|
||||
HighlightState::Off => {
|
||||
line = Line::from(Span::styled(
|
||||
&text,
|
||||
if is_active {
|
||||
Style::default().fg(theme.highlight)
|
||||
} else {
|
||||
Style::default().fg(theme.fg)
|
||||
},
|
||||
));
|
||||
}
|
||||
HighlightState::Characterwise { anchor } => {
|
||||
let (anchor_field, anchor_char) = *anchor;
|
||||
let start_field = min(anchor_field, *current_field_idx);
|
||||
let end_field = max(anchor_field, *current_field_idx);
|
||||
|
||||
let (start_char, end_char) = if anchor_field == *current_field_idx {
|
||||
(min(anchor_char, current_cursor_pos), max(anchor_char, current_cursor_pos))
|
||||
} else if anchor_field < *current_field_idx {
|
||||
(anchor_char, current_cursor_pos)
|
||||
} else {
|
||||
(current_cursor_pos, anchor_char)
|
||||
};
|
||||
|
||||
let highlight_style = Style::default().fg(theme.highlight).bg(theme.highlight_bg).add_modifier(Modifier::BOLD);
|
||||
let normal_style_in_highlight = Style::default().fg(theme.highlight);
|
||||
let normal_style_outside = Style::default().fg(theme.fg);
|
||||
|
||||
if i >= start_field && i <= end_field {
|
||||
if start_field == end_field {
|
||||
let clamped_start = start_char.min(text_len);
|
||||
let clamped_end = end_char.min(text_len);
|
||||
|
||||
let before: String = text.chars().take(clamped_start).collect();
|
||||
let highlighted: String = text.chars().skip(clamped_start).take(clamped_end.saturating_sub(clamped_start) + 1).collect();
|
||||
let after: String = text.chars().skip(clamped_end + 1).collect();
|
||||
|
||||
line = Line::from(vec![
|
||||
Span::styled(before, normal_style_in_highlight),
|
||||
Span::styled(highlighted, highlight_style),
|
||||
Span::styled(after, normal_style_in_highlight),
|
||||
]);
|
||||
} else if i == start_field {
|
||||
let safe_start = start_char.min(text_len);
|
||||
let before: String = text.chars().take(safe_start).collect();
|
||||
let highlighted: String = text.chars().skip(safe_start).collect();
|
||||
line = Line::from(vec![
|
||||
Span::styled(before, normal_style_in_highlight),
|
||||
Span::styled(highlighted, highlight_style),
|
||||
]);
|
||||
} else if i == end_field {
|
||||
let safe_end_inclusive = if text_len > 0 { end_char.min(text_len - 1) } else { 0 };
|
||||
let highlighted: String = text.chars().take(safe_end_inclusive + 1).collect();
|
||||
let after: String = text.chars().skip(safe_end_inclusive + 1).collect();
|
||||
line = Line::from(vec![
|
||||
Span::styled(highlighted, highlight_style),
|
||||
Span::styled(after, normal_style_in_highlight),
|
||||
]);
|
||||
} else {
|
||||
line = Line::from(Span::styled(&text, highlight_style));
|
||||
}
|
||||
} else {
|
||||
line = Line::from(Span::styled(
|
||||
&text,
|
||||
if is_active { normal_style_in_highlight } else { normal_style_outside }
|
||||
));
|
||||
}
|
||||
}
|
||||
HighlightState::Linewise { anchor_line } => {
|
||||
let start_field = min(*anchor_line, *current_field_idx);
|
||||
let end_field = max(*anchor_line, *current_field_idx);
|
||||
let highlight_style = Style::default().fg(theme.highlight).bg(theme.highlight_bg).add_modifier(Modifier::BOLD);
|
||||
let normal_style_in_highlight = Style::default().fg(theme.highlight);
|
||||
let normal_style_outside = Style::default().fg(theme.fg);
|
||||
|
||||
if i >= start_field && i <= end_field {
|
||||
line = Line::from(Span::styled(&text, highlight_style));
|
||||
} else {
|
||||
line = Line::from(Span::styled(
|
||||
&text,
|
||||
if is_active { normal_style_in_highlight } else { normal_style_outside }
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let input_display = Paragraph::new(line).alignment(Alignment::Left);
|
||||
f.render_widget(input_display, input_rows[i]);
|
||||
|
||||
if is_active {
|
||||
active_field_input_rect = Some(input_rows[i]);
|
||||
|
||||
// Use the provided closure to check for display override
|
||||
let cursor_x = if has_display_override(i) {
|
||||
// If an override exists, place the cursor at the end.
|
||||
input_rows[i].x + text.chars().count() as u16
|
||||
} else {
|
||||
// Otherwise, use the real cursor position.
|
||||
input_rows[i].x + current_cursor_pos as u16
|
||||
};
|
||||
let cursor_y = input_rows[i].y;
|
||||
f.set_cursor_position((cursor_x, cursor_y));
|
||||
}
|
||||
}
|
||||
|
||||
active_field_input_rect
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
// src/functions/modes.rs
|
||||
|
||||
pub mod read_only;
|
||||
pub mod edit;
|
||||
pub mod navigation;
|
||||
|
||||
pub use read_only::*;
|
||||
pub use edit::*;
|
||||
pub use navigation::*;
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
// src/functions/modes/edit.rs
|
||||
|
||||
pub mod form_e;
|
||||
pub mod auth_e;
|
||||
pub mod add_table_e;
|
||||
pub mod add_logic_e;
|
||||
@@ -1,135 +0,0 @@
|
||||
// src/functions/modes/edit/add_logic_e.rs
|
||||
use crate::state::pages::add_logic::AddLogicState;
|
||||
use canvas::canvas::CanvasState;
|
||||
use anyhow::Result;
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
|
||||
pub async fn execute_edit_action(
|
||||
action: &str,
|
||||
key: KeyEvent, // Keep key for insert_char
|
||||
state: &mut AddLogicState,
|
||||
ideal_cursor_column: &mut usize,
|
||||
) -> Result<String> {
|
||||
let mut message = String::new();
|
||||
|
||||
match action {
|
||||
"next_field" => {
|
||||
let current_field = state.current_field();
|
||||
let next_field = (current_field + 1) % AddLogicState::INPUT_FIELD_COUNT;
|
||||
state.set_current_field(next_field);
|
||||
*ideal_cursor_column = state.current_cursor_pos();
|
||||
message = format!("Focus on field {}", state.fields()[next_field]);
|
||||
}
|
||||
"prev_field" => {
|
||||
let current_field = state.current_field();
|
||||
let prev_field = if current_field == 0 {
|
||||
AddLogicState::INPUT_FIELD_COUNT - 1
|
||||
} else {
|
||||
current_field - 1
|
||||
};
|
||||
state.set_current_field(prev_field);
|
||||
*ideal_cursor_column = state.current_cursor_pos();
|
||||
message = format!("Focus on field {}", state.fields()[prev_field]);
|
||||
}
|
||||
"delete_char_forward" => {
|
||||
let current_pos = state.current_cursor_pos();
|
||||
let current_input_mut = state.get_current_input_mut();
|
||||
if current_pos < current_input_mut.len() {
|
||||
current_input_mut.remove(current_pos);
|
||||
state.set_has_unsaved_changes(true);
|
||||
if state.current_field() == 1 { state.update_target_column_suggestions(); }
|
||||
}
|
||||
}
|
||||
"delete_char_backward" => {
|
||||
let current_pos = state.current_cursor_pos();
|
||||
if current_pos > 0 {
|
||||
let new_pos = current_pos - 1;
|
||||
state.get_current_input_mut().remove(new_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
state.set_has_unsaved_changes(true);
|
||||
if state.current_field() == 1 { state.update_target_column_suggestions(); }
|
||||
}
|
||||
}
|
||||
"move_left" => {
|
||||
let current_pos = state.current_cursor_pos();
|
||||
if current_pos > 0 {
|
||||
let new_pos = current_pos - 1;
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
}
|
||||
"move_right" => {
|
||||
let current_pos = state.current_cursor_pos();
|
||||
let input_len = state.get_current_input().len();
|
||||
if current_pos < input_len {
|
||||
let new_pos = current_pos + 1;
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
}
|
||||
"insert_char" => {
|
||||
if let KeyCode::Char(c) = key.code {
|
||||
let current_pos = state.current_cursor_pos();
|
||||
state.get_current_input_mut().insert(current_pos, c);
|
||||
let new_pos = current_pos + 1;
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
state.set_has_unsaved_changes(true);
|
||||
if state.current_field() == 1 {
|
||||
state.update_target_column_suggestions();
|
||||
}
|
||||
}
|
||||
}
|
||||
"suggestion_down" => {
|
||||
if state.in_target_column_suggestion_mode && !state.target_column_suggestions.is_empty() {
|
||||
let current_selection = state.selected_target_column_suggestion_index.unwrap_or(0);
|
||||
let next_selection = (current_selection + 1) % state.target_column_suggestions.len();
|
||||
state.selected_target_column_suggestion_index = Some(next_selection);
|
||||
}
|
||||
}
|
||||
"suggestion_up" => {
|
||||
if state.in_target_column_suggestion_mode && !state.target_column_suggestions.is_empty() {
|
||||
let current_selection = state.selected_target_column_suggestion_index.unwrap_or(0);
|
||||
let prev_selection = if current_selection == 0 {
|
||||
state.target_column_suggestions.len() - 1
|
||||
} else {
|
||||
current_selection - 1
|
||||
};
|
||||
state.selected_target_column_suggestion_index = Some(prev_selection);
|
||||
}
|
||||
}
|
||||
"select_suggestion" => {
|
||||
if state.in_target_column_suggestion_mode {
|
||||
let mut selected_suggestion_text: Option<String> = None;
|
||||
|
||||
if let Some(selected_idx) = state.selected_target_column_suggestion_index {
|
||||
if let Some(suggestion) = state.target_column_suggestions.get(selected_idx) {
|
||||
selected_suggestion_text = Some(suggestion.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(suggestion_text) = selected_suggestion_text {
|
||||
state.target_column_input = suggestion_text.clone();
|
||||
state.target_column_cursor_pos = state.target_column_input.len();
|
||||
*ideal_cursor_column = state.target_column_cursor_pos;
|
||||
state.set_has_unsaved_changes(true);
|
||||
message = format!("Selected column: '{}'", suggestion_text);
|
||||
}
|
||||
|
||||
state.in_target_column_suggestion_mode = false;
|
||||
state.show_target_column_suggestions = false;
|
||||
state.selected_target_column_suggestion_index = None;
|
||||
state.update_target_column_suggestions();
|
||||
} else {
|
||||
let current_field = state.current_field();
|
||||
let next_field = (current_field + 1) % AddLogicState::INPUT_FIELD_COUNT;
|
||||
state.set_current_field(next_field);
|
||||
*ideal_cursor_column = state.current_cursor_pos();
|
||||
message = format!("Focus on field {}", state.fields()[next_field]);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(message)
|
||||
}
|
||||
@@ -1,341 +0,0 @@
|
||||
// src/functions/modes/edit/add_table_e.rs
|
||||
use crate::state::pages::add_table::AddTableState;
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use canvas::canvas::CanvasState;
|
||||
use anyhow::Result;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum CharType {
|
||||
Whitespace,
|
||||
Alphanumeric,
|
||||
Punctuation,
|
||||
}
|
||||
|
||||
fn get_char_type(c: char) -> CharType {
|
||||
if c.is_whitespace() {
|
||||
CharType::Whitespace
|
||||
} else if c.is_alphanumeric() {
|
||||
CharType::Alphanumeric
|
||||
} else {
|
||||
CharType::Punctuation
|
||||
}
|
||||
}
|
||||
|
||||
fn find_next_word_start(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 || current_pos >= len {
|
||||
return len;
|
||||
}
|
||||
|
||||
let mut pos = current_pos;
|
||||
let initial_type = get_char_type(chars[pos]);
|
||||
|
||||
while pos < len && get_char_type(chars[pos]) == initial_type {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
while pos < len && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
pos
|
||||
}
|
||||
|
||||
fn find_word_end(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut pos = current_pos.min(len - 1);
|
||||
|
||||
if get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
pos = find_next_word_start(text, pos);
|
||||
}
|
||||
|
||||
if pos >= len {
|
||||
return len.saturating_sub(1);
|
||||
}
|
||||
|
||||
let word_type = get_char_type(chars[pos]);
|
||||
while pos < len && get_char_type(chars[pos]) == word_type {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
pos.saturating_sub(1).min(len.saturating_sub(1))
|
||||
}
|
||||
|
||||
fn find_prev_word_start(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
if chars.is_empty() || current_pos == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut pos = current_pos.saturating_sub(1);
|
||||
|
||||
while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
if pos == 0 && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let word_type = get_char_type(chars[pos]);
|
||||
while pos > 0 && get_char_type(chars[pos - 1]) == word_type {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
pos
|
||||
}
|
||||
|
||||
fn find_prev_word_end(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 || current_pos == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut pos = current_pos.saturating_sub(1);
|
||||
|
||||
while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
if pos == 0 && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
return 0;
|
||||
}
|
||||
if pos == 0 && get_char_type(chars[pos]) != CharType::Whitespace {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let word_type = get_char_type(chars[pos]);
|
||||
while pos > 0 && get_char_type(chars[pos - 1]) == word_type {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
while pos > 0 && get_char_type(chars[pos - 1]) == CharType::Whitespace {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
if pos > 0 {
|
||||
pos - 1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes edit actions for the AddTable view canvas.
|
||||
pub async fn execute_edit_action(
|
||||
action: &str,
|
||||
key: KeyEvent, // Needed for insert_char
|
||||
state: &mut AddTableState,
|
||||
ideal_cursor_column: &mut usize,
|
||||
// Add other params like grpc_client if needed for future actions (e.g., validation)
|
||||
) -> Result<String> {
|
||||
// Use the CanvasState trait methods implemented for AddTableState
|
||||
match action {
|
||||
"insert_char" => {
|
||||
if let KeyCode::Char(c) = key.code {
|
||||
let cursor_pos = state.current_cursor_pos();
|
||||
let field_value = state.get_current_input_mut();
|
||||
let mut chars: Vec<char> = field_value.chars().collect();
|
||||
if cursor_pos <= chars.len() {
|
||||
chars.insert(cursor_pos, c);
|
||||
*field_value = chars.into_iter().collect();
|
||||
state.set_current_cursor_pos(cursor_pos + 1);
|
||||
state.set_has_unsaved_changes(true);
|
||||
*ideal_cursor_column = state.current_cursor_pos();
|
||||
}
|
||||
} else {
|
||||
return Ok("Error: insert_char called without a char key.".to_string());
|
||||
}
|
||||
Ok("".to_string()) // No message needed for char insertion
|
||||
}
|
||||
"delete_char_backward" => {
|
||||
if state.current_cursor_pos() > 0 {
|
||||
let cursor_pos = state.current_cursor_pos();
|
||||
let field_value = state.get_current_input_mut();
|
||||
let mut chars: Vec<char> = field_value.chars().collect();
|
||||
if cursor_pos <= chars.len() {
|
||||
chars.remove(cursor_pos - 1);
|
||||
*field_value = chars.into_iter().collect();
|
||||
let new_pos = cursor_pos - 1;
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
state.set_has_unsaved_changes(true);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"delete_char_forward" => {
|
||||
let cursor_pos = state.current_cursor_pos();
|
||||
let field_value = state.get_current_input_mut();
|
||||
let mut chars: Vec<char> = field_value.chars().collect();
|
||||
if cursor_pos < chars.len() {
|
||||
chars.remove(cursor_pos);
|
||||
*field_value = chars.into_iter().collect();
|
||||
state.set_has_unsaved_changes(true);
|
||||
*ideal_cursor_column = cursor_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"next_field" => {
|
||||
let num_fields = AddTableState::INPUT_FIELD_COUNT;
|
||||
if num_fields > 0 {
|
||||
let current_field = state.current_field();
|
||||
let last_field_index = num_fields - 1;
|
||||
// Prevent cycling forward
|
||||
if current_field < last_field_index {
|
||||
state.set_current_field(current_field + 1);
|
||||
}
|
||||
let current_input = state.get_current_input();
|
||||
let max_pos = current_input.len();
|
||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"prev_field" => {
|
||||
let num_fields = AddTableState::INPUT_FIELD_COUNT;
|
||||
if num_fields > 0 {
|
||||
let current_field = state.current_field();
|
||||
if current_field > 0 {
|
||||
state.set_current_field(current_field - 1);
|
||||
}
|
||||
let current_input = state.get_current_input();
|
||||
let max_pos = current_input.len();
|
||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_left" => {
|
||||
let new_pos = state.current_cursor_pos().saturating_sub(1);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_right" => {
|
||||
let current_input = state.get_current_input();
|
||||
let current_pos = state.current_cursor_pos();
|
||||
if current_pos < current_input.len() {
|
||||
let new_pos = current_pos + 1;
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_up" => {
|
||||
let current_field = state.current_field();
|
||||
// Prevent moving up from the first field
|
||||
if current_field > 0 {
|
||||
let new_field = current_field - 1;
|
||||
state.set_current_field(new_field);
|
||||
let current_input = state.get_current_input();
|
||||
let max_pos = current_input.len();
|
||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
||||
}
|
||||
Ok("ahoj".to_string())
|
||||
}
|
||||
"move_down" => {
|
||||
let num_fields = AddTableState::INPUT_FIELD_COUNT;
|
||||
if num_fields > 0 {
|
||||
let current_field = state.current_field();
|
||||
let last_field_index = num_fields - 1;
|
||||
if current_field < last_field_index {
|
||||
let new_field = current_field + 1;
|
||||
state.set_current_field(new_field);
|
||||
let current_input = state.get_current_input();
|
||||
let max_pos = current_input.len();
|
||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
||||
}
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_line_start" => {
|
||||
state.set_current_cursor_pos(0);
|
||||
*ideal_cursor_column = 0;
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_line_end" => {
|
||||
let current_input = state.get_current_input();
|
||||
let new_pos = current_input.len();
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_first_line" => {
|
||||
if AddTableState::INPUT_FIELD_COUNT > 0 {
|
||||
state.set_current_field(0);
|
||||
let current_input = state.get_current_input();
|
||||
let max_pos = current_input.len();
|
||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_last_line" => {
|
||||
let num_fields = AddTableState::INPUT_FIELD_COUNT;
|
||||
if num_fields > 0 {
|
||||
let new_field = num_fields - 1;
|
||||
state.set_current_field(new_field);
|
||||
let current_input = state.get_current_input();
|
||||
let max_pos = current_input.len();
|
||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_word_next" => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let new_pos = find_next_word_start(current_input, state.current_cursor_pos());
|
||||
let final_pos = new_pos.min(current_input.len());
|
||||
state.set_current_cursor_pos(final_pos);
|
||||
*ideal_cursor_column = final_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_word_end" => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let current_pos = state.current_cursor_pos();
|
||||
let new_pos = find_word_end(current_input, current_pos);
|
||||
|
||||
let final_pos = if new_pos == current_pos {
|
||||
find_word_end(current_input, new_pos + 1)
|
||||
} else {
|
||||
new_pos
|
||||
};
|
||||
|
||||
let max_valid_index = current_input.len().saturating_sub(1);
|
||||
let clamped_pos = final_pos.min(max_valid_index);
|
||||
state.set_current_cursor_pos(clamped_pos);
|
||||
*ideal_cursor_column = clamped_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_word_prev" => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let new_pos = find_prev_word_start(current_input, state.current_cursor_pos());
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_word_end_prev" => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let new_pos = find_prev_word_end(current_input, state.current_cursor_pos());
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
// Actions handled by main event loop (mode changes, save, revert)
|
||||
"exit_edit_mode" | "save" | "revert" => {
|
||||
Ok("Action handled by main loop".to_string())
|
||||
}
|
||||
_ => Ok(format!("Unknown or unhandled edit action: {}", action)),
|
||||
}
|
||||
}
|
||||
@@ -1,466 +0,0 @@
|
||||
// src/functions/modes/edit/auth_e.rs
|
||||
|
||||
use crate::services::grpc_client::GrpcClient;
|
||||
use crate::state::pages::form::FormState;
|
||||
use crate::state::pages::auth::RegisterState;
|
||||
use crate::state::app::state::AppState;
|
||||
use crate::tui::functions::common::form::{revert, save};
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use canvas::autocomplete::AutocompleteCanvasState;
|
||||
use canvas::canvas::CanvasState;
|
||||
use std::any::Any;
|
||||
use anyhow::Result;
|
||||
|
||||
pub async fn execute_common_action<S: CanvasState + Any>(
|
||||
action: &str,
|
||||
state: &mut S,
|
||||
grpc_client: &mut GrpcClient,
|
||||
app_state: &AppState,
|
||||
current_position: &mut u64,
|
||||
total_count: u64,
|
||||
) -> Result<String> {
|
||||
match action {
|
||||
"save" | "revert" => {
|
||||
if !state.has_unsaved_changes() {
|
||||
return Ok("No changes to save or revert.".to_string());
|
||||
}
|
||||
if let Some(form_state) =
|
||||
(state as &mut dyn Any).downcast_mut::<FormState>()
|
||||
{
|
||||
match action {
|
||||
"save" => {
|
||||
let outcome = save(
|
||||
app_state,
|
||||
form_state,
|
||||
grpc_client,
|
||||
)
|
||||
.await?;
|
||||
let message = format!("Save successful: {:?}", outcome); // Simple message for now
|
||||
Ok(message)
|
||||
}
|
||||
"revert" => {
|
||||
revert(
|
||||
form_state,
|
||||
grpc_client,
|
||||
)
|
||||
.await
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
} else {
|
||||
Ok(format!(
|
||||
"Action '{}' not implemented for this state type.",
|
||||
action
|
||||
))
|
||||
}
|
||||
}
|
||||
_ => Ok(format!("Common action '{}' not handled here.", action)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn execute_edit_action<S: CanvasState + Any + Send>(
|
||||
action: &str,
|
||||
key: KeyEvent,
|
||||
state: &mut S,
|
||||
ideal_cursor_column: &mut usize,
|
||||
) -> Result<String> {
|
||||
match action {
|
||||
"insert_char" => {
|
||||
if let KeyCode::Char(c) = key.code {
|
||||
let cursor_pos = state.current_cursor_pos();
|
||||
let field_value = state.get_current_input_mut();
|
||||
let mut chars: Vec<char> = field_value.chars().collect();
|
||||
if cursor_pos <= chars.len() {
|
||||
chars.insert(cursor_pos, c);
|
||||
*field_value = chars.into_iter().collect();
|
||||
state.set_current_cursor_pos(cursor_pos + 1);
|
||||
state.set_has_unsaved_changes(true);
|
||||
*ideal_cursor_column = state.current_cursor_pos();
|
||||
}
|
||||
} else {
|
||||
return Ok("Error: insert_char called without a char key."
|
||||
.to_string());
|
||||
}
|
||||
Ok("working?".to_string())
|
||||
}
|
||||
|
||||
"delete_char_backward" => {
|
||||
if state.current_cursor_pos() > 0 {
|
||||
let cursor_pos = state.current_cursor_pos();
|
||||
let field_value = state.get_current_input_mut();
|
||||
let mut chars: Vec<char> = field_value.chars().collect();
|
||||
if cursor_pos <= chars.len() {
|
||||
chars.remove(cursor_pos - 1);
|
||||
*field_value = chars.into_iter().collect();
|
||||
let new_pos = cursor_pos - 1;
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
state.set_has_unsaved_changes(true);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"delete_char_forward" => {
|
||||
let cursor_pos = state.current_cursor_pos();
|
||||
let field_value = state.get_current_input_mut();
|
||||
let mut chars: Vec<char> = field_value.chars().collect();
|
||||
if cursor_pos < chars.len() {
|
||||
chars.remove(cursor_pos);
|
||||
*field_value = chars.into_iter().collect();
|
||||
state.set_has_unsaved_changes(true);
|
||||
*ideal_cursor_column = cursor_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"next_field" => {
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields > 0 {
|
||||
let current_field = state.current_field();
|
||||
let new_field = (current_field + 1).min(num_fields - 1);
|
||||
state.set_current_field(new_field);
|
||||
let current_input = state.get_current_input();
|
||||
let max_pos = current_input.len();
|
||||
state.set_current_cursor_pos(
|
||||
(*ideal_cursor_column).min(max_pos),
|
||||
);
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"prev_field" => {
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields > 0 {
|
||||
let current_field = state.current_field();
|
||||
let new_field = current_field.saturating_sub(1);
|
||||
state.set_current_field(new_field);
|
||||
let current_input = state.get_current_input();
|
||||
let max_pos = current_input.len();
|
||||
state.set_current_cursor_pos(
|
||||
(*ideal_cursor_column).min(max_pos),
|
||||
);
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"move_left" => {
|
||||
let new_pos = state.current_cursor_pos().saturating_sub(1);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"move_right" => {
|
||||
let current_input = state.get_current_input();
|
||||
let current_pos = state.current_cursor_pos();
|
||||
if current_pos < current_input.len() {
|
||||
let new_pos = current_pos + 1;
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"move_up" => {
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields > 0 {
|
||||
let current_field = state.current_field();
|
||||
let new_field = current_field.saturating_sub(1);
|
||||
state.set_current_field(new_field);
|
||||
let current_input = state.get_current_input();
|
||||
let max_pos = current_input.len();
|
||||
state.set_current_cursor_pos(
|
||||
(*ideal_cursor_column).min(max_pos),
|
||||
);
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"move_down" => {
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields > 0 {
|
||||
let new_field = (state.current_field() + 1).min(num_fields - 1);
|
||||
state.set_current_field(new_field);
|
||||
let current_input = state.get_current_input();
|
||||
let max_pos = current_input.len();
|
||||
state.set_current_cursor_pos(
|
||||
(*ideal_cursor_column).min(max_pos),
|
||||
);
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"move_line_start" => {
|
||||
state.set_current_cursor_pos(0);
|
||||
*ideal_cursor_column = 0;
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"move_line_end" => {
|
||||
let current_input = state.get_current_input();
|
||||
let new_pos = current_input.len();
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"move_first_line" => {
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields > 0 {
|
||||
state.set_current_field(0);
|
||||
let current_input = state.get_current_input();
|
||||
let max_pos = current_input.len();
|
||||
state.set_current_cursor_pos(
|
||||
(*ideal_cursor_column).min(max_pos),
|
||||
);
|
||||
}
|
||||
Ok("Moved to first field".to_string())
|
||||
}
|
||||
|
||||
"move_last_line" => {
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields > 0 {
|
||||
let new_field = num_fields - 1;
|
||||
state.set_current_field(new_field);
|
||||
let current_input = state.get_current_input();
|
||||
let max_pos = current_input.len();
|
||||
state.set_current_cursor_pos(
|
||||
(*ideal_cursor_column).min(max_pos),
|
||||
);
|
||||
}
|
||||
Ok("Moved to last field".to_string())
|
||||
}
|
||||
|
||||
"move_word_next" => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let new_pos = find_next_word_start(
|
||||
current_input,
|
||||
state.current_cursor_pos(),
|
||||
);
|
||||
let final_pos = new_pos.min(current_input.len());
|
||||
state.set_current_cursor_pos(final_pos);
|
||||
*ideal_cursor_column = final_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"move_word_end" => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let current_pos = state.current_cursor_pos();
|
||||
let new_pos = find_word_end(current_input, current_pos);
|
||||
|
||||
let final_pos = if new_pos == current_pos {
|
||||
find_word_end(current_input, new_pos + 1)
|
||||
} else {
|
||||
new_pos
|
||||
};
|
||||
|
||||
let max_valid_index = current_input.len().saturating_sub(1);
|
||||
let clamped_pos = final_pos.min(max_valid_index);
|
||||
state.set_current_cursor_pos(clamped_pos);
|
||||
*ideal_cursor_column = clamped_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"move_word_prev" => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let new_pos = find_prev_word_start(
|
||||
current_input,
|
||||
state.current_cursor_pos(),
|
||||
);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"move_word_end_prev" => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let new_pos = find_prev_word_end(
|
||||
current_input,
|
||||
state.current_cursor_pos(),
|
||||
);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
Ok("Moved to previous word end".to_string())
|
||||
}
|
||||
|
||||
// --- Autocomplete Actions ---
|
||||
"suggestion_down" | "suggestion_up" | "select_suggestion" | "exit_suggestion_mode" => {
|
||||
// Attempt to downcast to RegisterState to handle suggestion logic here
|
||||
if let Some(register_state) = (state as &mut dyn Any).downcast_mut::<RegisterState>() {
|
||||
// Only handle if it's the role field (index 4) and autocomplete is active
|
||||
if register_state.current_field() == 4 && register_state.is_autocomplete_active() {
|
||||
match action {
|
||||
"suggestion_down" => {
|
||||
if let Some(autocomplete_state) = register_state.autocomplete_state_mut() {
|
||||
autocomplete_state.select_next();
|
||||
Ok("Suggestion changed down".to_string())
|
||||
} else {
|
||||
Ok("No autocomplete state".to_string())
|
||||
}
|
||||
}
|
||||
"suggestion_up" => {
|
||||
if let Some(autocomplete_state) = register_state.autocomplete_state_mut() {
|
||||
autocomplete_state.select_previous();
|
||||
Ok("Suggestion changed up".to_string())
|
||||
} else {
|
||||
Ok("No autocomplete state".to_string())
|
||||
}
|
||||
}
|
||||
"select_suggestion" => {
|
||||
if let Some(message) = register_state.apply_autocomplete_selection() {
|
||||
Ok(message)
|
||||
} else {
|
||||
Ok("No suggestion selected".to_string())
|
||||
}
|
||||
}
|
||||
"exit_suggestion_mode" => {
|
||||
register_state.deactivate_autocomplete();
|
||||
Ok("Suggestions hidden".to_string())
|
||||
}
|
||||
_ => Ok("Suggestion action ignored: State mismatch.".to_string())
|
||||
}
|
||||
} else {
|
||||
Ok("Suggestion action ignored: Not on role field or autocomplete not active.".to_string())
|
||||
}
|
||||
} else {
|
||||
Ok(format!("Action '{}' not applicable for this state type.", action))
|
||||
}
|
||||
}
|
||||
// --- End Autocomplete Actions ---
|
||||
|
||||
|
||||
_ => Ok(format!("Unknown or unhandled edit action: {}", action)),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum CharType {
|
||||
Whitespace,
|
||||
Alphanumeric,
|
||||
Punctuation,
|
||||
}
|
||||
|
||||
fn get_char_type(c: char) -> CharType {
|
||||
if c.is_whitespace() {
|
||||
CharType::Whitespace
|
||||
} else if c.is_alphanumeric() {
|
||||
CharType::Alphanumeric
|
||||
} else {
|
||||
CharType::Punctuation
|
||||
}
|
||||
}
|
||||
|
||||
fn find_next_word_start(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 || current_pos >= len {
|
||||
return len;
|
||||
}
|
||||
|
||||
let mut pos = current_pos;
|
||||
let initial_type = get_char_type(chars[pos]);
|
||||
|
||||
while pos < len && get_char_type(chars[pos]) == initial_type {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
while pos < len && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
pos
|
||||
}
|
||||
|
||||
fn find_word_end(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut pos = current_pos.min(len - 1);
|
||||
|
||||
if get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
pos = find_next_word_start(text, pos);
|
||||
}
|
||||
|
||||
if pos >= len {
|
||||
return len.saturating_sub(1);
|
||||
}
|
||||
|
||||
let word_type = get_char_type(chars[pos]);
|
||||
while pos < len && get_char_type(chars[pos]) == word_type {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
pos.saturating_sub(1).min(len.saturating_sub(1))
|
||||
}
|
||||
|
||||
fn find_prev_word_start(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
if chars.is_empty() || current_pos == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut pos = current_pos.saturating_sub(1);
|
||||
|
||||
while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
if pos == 0 && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let word_type = get_char_type(chars[pos]);
|
||||
while pos > 0 && get_char_type(chars[pos - 1]) == word_type {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
pos
|
||||
}
|
||||
|
||||
fn find_prev_word_end(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 || current_pos == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut pos = current_pos.saturating_sub(1);
|
||||
|
||||
while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
if pos == 0 && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
return 0;
|
||||
}
|
||||
if pos == 0 && get_char_type(chars[pos]) != CharType::Whitespace {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let word_type = get_char_type(chars[pos]);
|
||||
while pos > 0 && get_char_type(chars[pos - 1]) == word_type {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
while pos > 0 && get_char_type(chars[pos - 1]) == CharType::Whitespace {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
if pos > 0 {
|
||||
pos - 1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
@@ -1,435 +0,0 @@
|
||||
// src/functions/modes/edit/form_e.rs
|
||||
|
||||
use crate::services::grpc_client::GrpcClient;
|
||||
use crate::state::pages::form::FormState;
|
||||
use crate::state::app::state::AppState;
|
||||
use crate::tui::functions::common::form::{revert, save};
|
||||
use crate::tui::functions::common::form::SaveOutcome;
|
||||
use crate::modes::handlers::event::EventOutcome;
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use canvas::canvas::CanvasState;
|
||||
use std::any::Any;
|
||||
use anyhow::Result;
|
||||
|
||||
pub async fn execute_common_action<S: CanvasState + Any>(
|
||||
action: &str,
|
||||
state: &mut S,
|
||||
grpc_client: &mut GrpcClient,
|
||||
app_state: &AppState,
|
||||
) -> Result<EventOutcome> {
|
||||
match action {
|
||||
"save" | "revert" => {
|
||||
if !state.has_unsaved_changes() {
|
||||
return Ok(EventOutcome::Ok("No changes to save or revert.".to_string()));
|
||||
}
|
||||
if let Some(form_state) =
|
||||
(state as &mut dyn Any).downcast_mut::<FormState>()
|
||||
{
|
||||
match action {
|
||||
"save" => {
|
||||
let save_result = save(
|
||||
app_state,
|
||||
form_state,
|
||||
grpc_client,
|
||||
).await;
|
||||
|
||||
match save_result {
|
||||
Ok(save_outcome) => {
|
||||
let message = match save_outcome {
|
||||
SaveOutcome::NoChange => "No changes to save.".to_string(),
|
||||
SaveOutcome::UpdatedExisting => "Entry updated.".to_string(),
|
||||
SaveOutcome::CreatedNew(_) => "New entry created.".to_string(),
|
||||
};
|
||||
Ok(EventOutcome::DataSaved(save_outcome, message))
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
"revert" => {
|
||||
let revert_result = revert(
|
||||
form_state,
|
||||
grpc_client,
|
||||
).await;
|
||||
|
||||
match revert_result {
|
||||
Ok(message) => Ok(EventOutcome::Ok(message)),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
} else {
|
||||
Ok(EventOutcome::Ok(format!(
|
||||
"Action '{}' not implemented for this state type.",
|
||||
action
|
||||
)))
|
||||
}
|
||||
}
|
||||
_ => Ok(EventOutcome::Ok(format!("Common action '{}' not handled here.", action))),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn execute_edit_action<S: CanvasState>(
|
||||
action: &str,
|
||||
key: KeyEvent,
|
||||
state: &mut S,
|
||||
ideal_cursor_column: &mut usize,
|
||||
) -> Result<String> {
|
||||
match action {
|
||||
"insert_char" => {
|
||||
if let KeyCode::Char(c) = key.code {
|
||||
let cursor_pos = state.current_cursor_pos();
|
||||
let field_value = state.get_current_input_mut();
|
||||
let mut chars: Vec<char> = field_value.chars().collect();
|
||||
if cursor_pos <= chars.len() {
|
||||
chars.insert(cursor_pos, c);
|
||||
*field_value = chars.into_iter().collect();
|
||||
state.set_current_cursor_pos(cursor_pos + 1);
|
||||
state.set_has_unsaved_changes(true);
|
||||
*ideal_cursor_column = state.current_cursor_pos();
|
||||
}
|
||||
} else {
|
||||
return Ok("Error: insert_char called without a char key."
|
||||
.to_string());
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"delete_char_backward" => {
|
||||
if state.current_cursor_pos() > 0 {
|
||||
let cursor_pos = state.current_cursor_pos();
|
||||
let field_value = state.get_current_input_mut();
|
||||
let mut chars: Vec<char> = field_value.chars().collect();
|
||||
if cursor_pos <= chars.len() {
|
||||
chars.remove(cursor_pos - 1);
|
||||
*field_value = chars.into_iter().collect();
|
||||
let new_pos = cursor_pos - 1;
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
state.set_has_unsaved_changes(true);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"delete_char_forward" => {
|
||||
let cursor_pos = state.current_cursor_pos();
|
||||
let field_value = state.get_current_input_mut();
|
||||
let mut chars: Vec<char> = field_value.chars().collect();
|
||||
if cursor_pos < chars.len() {
|
||||
chars.remove(cursor_pos);
|
||||
*field_value = chars.into_iter().collect();
|
||||
state.set_has_unsaved_changes(true);
|
||||
*ideal_cursor_column = cursor_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"next_field" => {
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields > 0 {
|
||||
let current_field = state.current_field();
|
||||
let new_field = (current_field + 1) % num_fields;
|
||||
state.set_current_field(new_field);
|
||||
let current_input = state.get_current_input();
|
||||
let max_pos = current_input.len();
|
||||
state.set_current_cursor_pos(
|
||||
(*ideal_cursor_column).min(max_pos),
|
||||
);
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"prev_field" => {
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields > 0 {
|
||||
let current_field = state.current_field();
|
||||
let new_field = if current_field == 0 {
|
||||
num_fields - 1
|
||||
} else {
|
||||
current_field - 1
|
||||
};
|
||||
state.set_current_field(new_field);
|
||||
let current_input = state.get_current_input();
|
||||
let max_pos = current_input.len();
|
||||
state.set_current_cursor_pos(
|
||||
(*ideal_cursor_column).min(max_pos),
|
||||
);
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"move_left" => {
|
||||
let new_pos = state.current_cursor_pos().saturating_sub(1);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"move_right" => {
|
||||
let current_input = state.get_current_input();
|
||||
let current_pos = state.current_cursor_pos();
|
||||
if current_pos < current_input.len() {
|
||||
let new_pos = current_pos + 1;
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"move_up" => {
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields > 0 {
|
||||
let current_field = state.current_field();
|
||||
let new_field = current_field.saturating_sub(1);
|
||||
state.set_current_field(new_field);
|
||||
let current_input = state.get_current_input();
|
||||
let max_pos = current_input.len();
|
||||
state.set_current_cursor_pos(
|
||||
(*ideal_cursor_column).min(max_pos),
|
||||
);
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"move_down" => {
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields > 0 {
|
||||
let new_field = (state.current_field() + 1).min(num_fields - 1);
|
||||
state.set_current_field(new_field);
|
||||
let current_input = state.get_current_input();
|
||||
let max_pos = current_input.len();
|
||||
state.set_current_cursor_pos(
|
||||
(*ideal_cursor_column).min(max_pos),
|
||||
);
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"move_line_start" => {
|
||||
state.set_current_cursor_pos(0);
|
||||
*ideal_cursor_column = 0;
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"move_line_end" => {
|
||||
let current_input = state.get_current_input();
|
||||
let new_pos = current_input.len();
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"move_first_line" => {
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields > 0 {
|
||||
state.set_current_field(0);
|
||||
let current_input = state.get_current_input();
|
||||
let max_pos = current_input.len();
|
||||
state.set_current_cursor_pos(
|
||||
(*ideal_cursor_column).min(max_pos),
|
||||
);
|
||||
}
|
||||
Ok("Moved to first field".to_string())
|
||||
}
|
||||
|
||||
"move_last_line" => {
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields > 0 {
|
||||
let new_field = num_fields - 1;
|
||||
state.set_current_field(new_field);
|
||||
let current_input = state.get_current_input();
|
||||
let max_pos = current_input.len();
|
||||
state.set_current_cursor_pos(
|
||||
(*ideal_cursor_column).min(max_pos),
|
||||
);
|
||||
}
|
||||
Ok("Moved to last field".to_string())
|
||||
}
|
||||
|
||||
"move_word_next" => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let new_pos = find_next_word_start(
|
||||
current_input,
|
||||
state.current_cursor_pos(),
|
||||
);
|
||||
let final_pos = new_pos.min(current_input.len());
|
||||
state.set_current_cursor_pos(final_pos);
|
||||
*ideal_cursor_column = final_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"move_word_end" => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let current_pos = state.current_cursor_pos();
|
||||
let new_pos = find_word_end(current_input, current_pos);
|
||||
|
||||
let final_pos = if new_pos == current_pos {
|
||||
find_word_end(current_input, new_pos + 1)
|
||||
} else {
|
||||
new_pos
|
||||
};
|
||||
|
||||
let max_valid_index = current_input.len().saturating_sub(1);
|
||||
let clamped_pos = final_pos.min(max_valid_index);
|
||||
state.set_current_cursor_pos(clamped_pos);
|
||||
*ideal_cursor_column = clamped_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"move_word_prev" => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let new_pos = find_prev_word_start(
|
||||
current_input,
|
||||
state.current_cursor_pos(),
|
||||
);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"move_word_end_prev" => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let new_pos = find_prev_word_end(
|
||||
current_input,
|
||||
state.current_cursor_pos(),
|
||||
);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
Ok("Moved to previous word end".to_string())
|
||||
}
|
||||
|
||||
_ => Ok(format!("Unknown or unhandled edit action: {}", action)),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum CharType {
|
||||
Whitespace,
|
||||
Alphanumeric,
|
||||
Punctuation,
|
||||
}
|
||||
|
||||
fn get_char_type(c: char) -> CharType {
|
||||
if c.is_whitespace() {
|
||||
CharType::Whitespace
|
||||
} else if c.is_alphanumeric() {
|
||||
CharType::Alphanumeric
|
||||
} else {
|
||||
CharType::Punctuation
|
||||
}
|
||||
}
|
||||
|
||||
fn find_next_word_start(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 || current_pos >= len {
|
||||
return len;
|
||||
}
|
||||
|
||||
let mut pos = current_pos;
|
||||
let initial_type = get_char_type(chars[pos]);
|
||||
|
||||
while pos < len && get_char_type(chars[pos]) == initial_type {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
while pos < len && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
pos
|
||||
}
|
||||
|
||||
fn find_word_end(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut pos = current_pos.min(len - 1);
|
||||
|
||||
if get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
pos = find_next_word_start(text, pos);
|
||||
}
|
||||
|
||||
if pos >= len {
|
||||
return len.saturating_sub(1);
|
||||
}
|
||||
|
||||
let word_type = get_char_type(chars[pos]);
|
||||
while pos < len && get_char_type(chars[pos]) == word_type {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
pos.saturating_sub(1).min(len.saturating_sub(1))
|
||||
}
|
||||
|
||||
fn find_prev_word_start(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
if chars.is_empty() || current_pos == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut pos = current_pos.saturating_sub(1);
|
||||
|
||||
while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
if pos == 0 && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let word_type = get_char_type(chars[pos]);
|
||||
while pos > 0 && get_char_type(chars[pos - 1]) == word_type {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
pos
|
||||
}
|
||||
|
||||
fn find_prev_word_end(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 || current_pos == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut pos = current_pos.saturating_sub(1);
|
||||
|
||||
while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
if pos == 0 && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
return 0;
|
||||
}
|
||||
if pos == 0 && get_char_type(chars[pos]) != CharType::Whitespace {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let word_type = get_char_type(chars[pos]);
|
||||
while pos > 0 && get_char_type(chars[pos - 1]) == word_type {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
while pos > 0 && get_char_type(chars[pos - 1]) == CharType::Whitespace {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
if pos > 0 {
|
||||
pos - 1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// src/functions/modes/read_only.rs
|
||||
|
||||
pub mod auth_ro;
|
||||
pub mod form_ro;
|
||||
pub mod add_table_ro;
|
||||
pub mod add_logic_ro;
|
||||
@@ -1,235 +0,0 @@
|
||||
// src/functions/modes/read_only/add_logic_ro.rs
|
||||
use crate::config::binds::key_sequences::KeySequenceTracker;
|
||||
use crate::state::pages::add_logic::AddLogicState; // Changed
|
||||
use crate::state::app::state::AppState;
|
||||
use canvas::canvas::CanvasState;
|
||||
use anyhow::Result;
|
||||
|
||||
// Word navigation helpers (get_char_type, find_next_word_start, etc.)
|
||||
// can be kept as they are generic.
|
||||
#[derive(PartialEq)]
|
||||
enum CharType {
|
||||
Whitespace,
|
||||
Alphanumeric,
|
||||
Punctuation,
|
||||
}
|
||||
|
||||
fn get_char_type(c: char) -> CharType {
|
||||
if c.is_whitespace() { CharType::Whitespace }
|
||||
else if c.is_alphanumeric() { CharType::Alphanumeric }
|
||||
else { CharType::Punctuation }
|
||||
}
|
||||
|
||||
fn find_next_word_start(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 || current_pos >= len { return len; }
|
||||
let mut pos = current_pos;
|
||||
let initial_type = get_char_type(chars[pos]);
|
||||
while pos < len && get_char_type(chars[pos]) == initial_type { pos += 1; }
|
||||
while pos < len && get_char_type(chars[pos]) == CharType::Whitespace { pos += 1; }
|
||||
pos
|
||||
}
|
||||
|
||||
fn find_word_end(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 { return 0; }
|
||||
let mut pos = current_pos.min(len - 1);
|
||||
if get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
pos = find_next_word_start(text, pos);
|
||||
}
|
||||
if pos >= len { return len.saturating_sub(1); }
|
||||
let word_type = get_char_type(chars[pos]);
|
||||
while pos < len && get_char_type(chars[pos]) == word_type { pos += 1; }
|
||||
pos.saturating_sub(1).min(len.saturating_sub(1))
|
||||
}
|
||||
|
||||
fn find_prev_word_start(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
if chars.is_empty() || current_pos == 0 { return 0; }
|
||||
let mut pos = current_pos.saturating_sub(1);
|
||||
while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace { pos -= 1; }
|
||||
if pos == 0 && get_char_type(chars[pos]) == CharType::Whitespace { return 0; }
|
||||
let word_type = get_char_type(chars[pos]);
|
||||
while pos > 0 && get_char_type(chars[pos - 1]) == word_type { pos -= 1; }
|
||||
pos
|
||||
}
|
||||
|
||||
fn find_prev_word_end(text: &str, current_pos: usize) -> usize {
|
||||
let prev_start = find_prev_word_start(text, current_pos);
|
||||
if prev_start == 0 { return 0; }
|
||||
find_word_end(text, prev_start.saturating_sub(1))
|
||||
}
|
||||
|
||||
|
||||
/// Executes read-only actions for the AddLogic view canvas.
|
||||
pub async fn execute_action(
|
||||
action: &str,
|
||||
app_state: &mut AppState,
|
||||
state: &mut AddLogicState,
|
||||
ideal_cursor_column: &mut usize,
|
||||
key_sequence_tracker: &mut KeySequenceTracker,
|
||||
command_message: &mut String,
|
||||
) -> Result<String> {
|
||||
match action {
|
||||
"move_up" => {
|
||||
key_sequence_tracker.reset();
|
||||
let num_fields = AddLogicState::INPUT_FIELD_COUNT;
|
||||
if num_fields == 0 { return Ok("No fields.".to_string()); }
|
||||
let current_field = state.current_field();
|
||||
|
||||
if current_field > 0 {
|
||||
let new_field = current_field - 1;
|
||||
state.set_current_field(new_field);
|
||||
let current_input = state.get_current_input();
|
||||
let max_cursor_pos = if current_input.is_empty() { 0 } else { current_input.len().saturating_sub(1) };
|
||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
} else {
|
||||
*command_message = "At top of form.".to_string();
|
||||
}
|
||||
Ok(command_message.clone())
|
||||
}
|
||||
"move_down" => {
|
||||
key_sequence_tracker.reset();
|
||||
let num_fields = AddLogicState::INPUT_FIELD_COUNT;
|
||||
if num_fields == 0 { return Ok("No fields.".to_string()); }
|
||||
let current_field = state.current_field();
|
||||
let last_field_index = num_fields - 1;
|
||||
|
||||
if current_field < last_field_index {
|
||||
let new_field = current_field + 1;
|
||||
state.set_current_field(new_field);
|
||||
let current_input = state.get_current_input();
|
||||
let max_cursor_pos = if current_input.is_empty() { 0 } else { current_input.len().saturating_sub(1) };
|
||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
} else {
|
||||
// Move focus outside canvas when moving down from the last field
|
||||
// FIX: Go to ScriptContentPreview instead of SaveButton
|
||||
app_state.ui.focus_outside_canvas = true;
|
||||
state.last_canvas_field = 2;
|
||||
state.current_focus = crate::state::pages::add_logic::AddLogicFocus::ScriptContentPreview; // FIXED!
|
||||
*command_message = "Focus moved to script preview".to_string();
|
||||
}
|
||||
Ok(command_message.clone())
|
||||
}
|
||||
// ... (rest of the actions remain the same) ...
|
||||
"move_first_line" => {
|
||||
key_sequence_tracker.reset();
|
||||
if AddLogicState::INPUT_FIELD_COUNT > 0 {
|
||||
state.set_current_field(0);
|
||||
let current_input = state.get_current_input();
|
||||
let max_cursor_pos = if current_input.is_empty() { 0 } else { current_input.len().saturating_sub(1) };
|
||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_last_line" => {
|
||||
key_sequence_tracker.reset();
|
||||
let num_fields = AddLogicState::INPUT_FIELD_COUNT;
|
||||
if num_fields > 0 {
|
||||
let last_field_index = num_fields - 1;
|
||||
state.set_current_field(last_field_index);
|
||||
let current_input = state.get_current_input();
|
||||
let max_cursor_pos = if current_input.is_empty() { 0 } else { current_input.len().saturating_sub(1) };
|
||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_left" => {
|
||||
let current_pos = state.current_cursor_pos();
|
||||
let new_pos = current_pos.saturating_sub(1);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_right" => {
|
||||
let current_input = state.get_current_input();
|
||||
let current_pos = state.current_cursor_pos();
|
||||
if !current_input.is_empty() && current_pos < current_input.len().saturating_sub(1) {
|
||||
let new_pos = current_pos + 1;
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_word_next" => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let new_pos = find_next_word_start(current_input, state.current_cursor_pos());
|
||||
let final_pos = new_pos.min(current_input.len().saturating_sub(1));
|
||||
state.set_current_cursor_pos(final_pos);
|
||||
*ideal_cursor_column = final_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_word_end" => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let current_pos = state.current_cursor_pos();
|
||||
let new_pos = find_word_end(current_input, current_pos);
|
||||
let final_pos = if new_pos == current_pos && current_pos < current_input.len().saturating_sub(1) {
|
||||
find_word_end(current_input, current_pos + 1)
|
||||
} else {
|
||||
new_pos
|
||||
};
|
||||
let max_valid_index = current_input.len().saturating_sub(1);
|
||||
let clamped_pos = final_pos.min(max_valid_index);
|
||||
state.set_current_cursor_pos(clamped_pos);
|
||||
*ideal_cursor_column = clamped_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_word_prev" => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let new_pos = find_prev_word_start(current_input, state.current_cursor_pos());
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_word_end_prev" => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let new_pos = find_prev_word_end(current_input, state.current_cursor_pos());
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_line_start" => {
|
||||
state.set_current_cursor_pos(0);
|
||||
*ideal_cursor_column = 0;
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_line_end" => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let new_pos = current_input.len().saturating_sub(1);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
} else {
|
||||
state.set_current_cursor_pos(0);
|
||||
*ideal_cursor_column = 0;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"enter_edit_mode_before" | "enter_edit_mode_after" | "enter_command_mode" | "exit_highlight_mode" => {
|
||||
key_sequence_tracker.reset();
|
||||
Ok("Mode change handled by main loop".to_string())
|
||||
}
|
||||
_ => {
|
||||
key_sequence_tracker.reset();
|
||||
command_message.clear();
|
||||
Ok(format!("Unknown read-only action: {}", action))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,267 +0,0 @@
|
||||
// src/functions/modes/read_only/add_table_ro.rs
|
||||
use crate::config::binds::key_sequences::KeySequenceTracker;
|
||||
use crate::state::pages::add_table::AddTableState;
|
||||
use crate::state::app::state::AppState;
|
||||
use canvas::canvas::CanvasState;
|
||||
use anyhow::Result;
|
||||
|
||||
// Re-use word navigation helpers if they are public or move them to a common module
|
||||
// For now, duplicating them here for simplicity. Consider refactoring later.
|
||||
#[derive(PartialEq)]
|
||||
enum CharType {
|
||||
Whitespace,
|
||||
Alphanumeric,
|
||||
Punctuation,
|
||||
}
|
||||
|
||||
fn get_char_type(c: char) -> CharType {
|
||||
if c.is_whitespace() { CharType::Whitespace }
|
||||
else if c.is_alphanumeric() { CharType::Alphanumeric }
|
||||
else { CharType::Punctuation }
|
||||
}
|
||||
|
||||
fn find_next_word_start(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 || current_pos >= len { return len; }
|
||||
let mut pos = current_pos;
|
||||
let initial_type = get_char_type(chars[pos]);
|
||||
while pos < len && get_char_type(chars[pos]) == initial_type { pos += 1; }
|
||||
while pos < len && get_char_type(chars[pos]) == CharType::Whitespace { pos += 1; }
|
||||
pos
|
||||
}
|
||||
|
||||
fn find_word_end(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 { return 0; }
|
||||
let mut pos = current_pos.min(len - 1);
|
||||
if get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
pos = find_next_word_start(text, pos);
|
||||
}
|
||||
if pos >= len { return len.saturating_sub(1); }
|
||||
let word_type = get_char_type(chars[pos]);
|
||||
while pos < len && get_char_type(chars[pos]) == word_type { pos += 1; }
|
||||
pos.saturating_sub(1).min(len.saturating_sub(1))
|
||||
}
|
||||
|
||||
fn find_prev_word_start(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
if chars.is_empty() || current_pos == 0 { return 0; }
|
||||
let mut pos = current_pos.saturating_sub(1);
|
||||
while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace { pos -= 1; }
|
||||
if pos == 0 && get_char_type(chars[pos]) == CharType::Whitespace { return 0; }
|
||||
let word_type = get_char_type(chars[pos]);
|
||||
while pos > 0 && get_char_type(chars[pos - 1]) == word_type { pos -= 1; }
|
||||
pos
|
||||
}
|
||||
|
||||
// Note: find_prev_word_end might need adjustments based on desired behavior.
|
||||
// This version finds the end of the word *before* the previous word start.
|
||||
fn find_prev_word_end(text: &str, current_pos: usize) -> usize {
|
||||
let prev_start = find_prev_word_start(text, current_pos);
|
||||
if prev_start == 0 { return 0; }
|
||||
// Find the end of the word that starts at prev_start - 1
|
||||
find_word_end(text, prev_start.saturating_sub(1))
|
||||
}
|
||||
|
||||
|
||||
/// Executes read-only actions for the AddTable view canvas.
|
||||
pub async fn execute_action(
|
||||
action: &str,
|
||||
app_state: &mut AppState, // Needed for focus_outside_canvas
|
||||
state: &mut AddTableState,
|
||||
ideal_cursor_column: &mut usize,
|
||||
key_sequence_tracker: &mut KeySequenceTracker,
|
||||
command_message: &mut String, // Keep for potential messages
|
||||
) -> Result<String> {
|
||||
// Use the CanvasState trait methods implemented for AddTableState
|
||||
match action {
|
||||
"move_up" => {
|
||||
key_sequence_tracker.reset();
|
||||
let num_fields = AddTableState::INPUT_FIELD_COUNT;
|
||||
if num_fields == 0 {
|
||||
*command_message = "No fields.".to_string();
|
||||
return Ok(command_message.clone());
|
||||
}
|
||||
let current_field = state.current_field(); // Gets the index (0, 1, or 2)
|
||||
|
||||
if current_field > 0 {
|
||||
// This handles moving from field 2 -> 1, or 1 -> 0
|
||||
let new_field = current_field - 1;
|
||||
state.set_current_field(new_field);
|
||||
let current_input = state.get_current_input();
|
||||
let max_cursor_pos = current_input.len(); // Allow cursor at end
|
||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos; // Update ideal column as cursor moved
|
||||
*command_message = "".to_string(); // Clear message for successful internal navigation
|
||||
} else {
|
||||
// current_field is 0 (InputTableName), and user pressed Up.
|
||||
// Forbid moving up. Do not change focus or cursor.
|
||||
*command_message = "At top of form.".to_string();
|
||||
}
|
||||
Ok(command_message.clone())
|
||||
}
|
||||
"move_down" => {
|
||||
key_sequence_tracker.reset();
|
||||
let num_fields = AddTableState::INPUT_FIELD_COUNT;
|
||||
if num_fields == 0 {
|
||||
*command_message = "No fields.".to_string();
|
||||
return Ok(command_message.clone());
|
||||
}
|
||||
let current_field = state.current_field();
|
||||
let last_field_index = num_fields - 1;
|
||||
|
||||
if current_field < last_field_index {
|
||||
let new_field = current_field + 1;
|
||||
state.set_current_field(new_field);
|
||||
let current_input = state.get_current_input();
|
||||
let max_cursor_pos = current_input.len(); // Allow cursor at end
|
||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos; // Update ideal column
|
||||
*command_message = "".to_string();
|
||||
} else {
|
||||
// Move focus outside canvas when moving down from the last field
|
||||
app_state.ui.focus_outside_canvas = true;
|
||||
// Set focus to the first element outside canvas (AddColumnButton)
|
||||
state.current_focus =
|
||||
crate::state::pages::add_table::AddTableFocus::AddColumnButton;
|
||||
*command_message = "Focus moved below canvas".to_string();
|
||||
}
|
||||
Ok(command_message.clone())
|
||||
}
|
||||
// ... (other actions like "move_first_line", "move_left", etc. remain the same) ...
|
||||
"move_first_line" => {
|
||||
key_sequence_tracker.reset();
|
||||
if AddTableState::INPUT_FIELD_COUNT > 0 {
|
||||
state.set_current_field(0);
|
||||
let current_input = state.get_current_input();
|
||||
let max_cursor_pos = current_input.len();
|
||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos; // Update ideal column
|
||||
}
|
||||
*command_message = "".to_string();
|
||||
Ok(command_message.clone())
|
||||
}
|
||||
"move_last_line" => {
|
||||
key_sequence_tracker.reset();
|
||||
let num_fields = AddTableState::INPUT_FIELD_COUNT;
|
||||
if num_fields > 0 {
|
||||
let last_field_index = num_fields - 1;
|
||||
state.set_current_field(last_field_index);
|
||||
let current_input = state.get_current_input();
|
||||
let max_cursor_pos = current_input.len();
|
||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos; // Update ideal column
|
||||
}
|
||||
*command_message = "".to_string();
|
||||
Ok(command_message.clone())
|
||||
}
|
||||
"move_left" => {
|
||||
let current_pos = state.current_cursor_pos();
|
||||
let new_pos = current_pos.saturating_sub(1);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
*command_message = "".to_string();
|
||||
Ok(command_message.clone())
|
||||
}
|
||||
"move_right" => {
|
||||
let current_input = state.get_current_input();
|
||||
let current_pos = state.current_cursor_pos();
|
||||
// Allow moving cursor one position past the end
|
||||
if current_pos < current_input.len() {
|
||||
let new_pos = current_pos + 1;
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
*command_message = "".to_string();
|
||||
Ok(command_message.clone())
|
||||
}
|
||||
"move_word_next" => {
|
||||
let current_input = state.get_current_input();
|
||||
let new_pos = find_next_word_start(
|
||||
current_input,
|
||||
state.current_cursor_pos(),
|
||||
);
|
||||
let final_pos = new_pos.min(current_input.len()); // Allow cursor at end
|
||||
state.set_current_cursor_pos(final_pos);
|
||||
*ideal_cursor_column = final_pos;
|
||||
*command_message = "".to_string();
|
||||
Ok(command_message.clone())
|
||||
}
|
||||
"move_word_end" => {
|
||||
let current_input = state.get_current_input();
|
||||
let current_pos = state.current_cursor_pos();
|
||||
let new_pos = find_word_end(current_input, current_pos);
|
||||
// If find_word_end returns current_pos, try starting search from next char
|
||||
let final_pos =
|
||||
if new_pos == current_pos && current_pos < current_input.len() {
|
||||
find_word_end(current_input, current_pos + 1)
|
||||
} else {
|
||||
new_pos
|
||||
};
|
||||
let max_valid_index = current_input.len(); // Allow cursor at end
|
||||
let clamped_pos = final_pos.min(max_valid_index);
|
||||
state.set_current_cursor_pos(clamped_pos);
|
||||
*ideal_cursor_column = clamped_pos;
|
||||
*command_message = "".to_string();
|
||||
Ok(command_message.clone())
|
||||
}
|
||||
"move_word_prev" => {
|
||||
let current_input = state.get_current_input();
|
||||
let new_pos = find_prev_word_start(
|
||||
current_input,
|
||||
state.current_cursor_pos(),
|
||||
);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
*command_message = "".to_string();
|
||||
Ok(command_message.clone())
|
||||
}
|
||||
"move_word_end_prev" => {
|
||||
let current_input = state.get_current_input();
|
||||
let new_pos = find_prev_word_end(
|
||||
current_input,
|
||||
state.current_cursor_pos(),
|
||||
);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
*command_message = "".to_string();
|
||||
Ok(command_message.clone())
|
||||
}
|
||||
"move_line_start" => {
|
||||
state.set_current_cursor_pos(0);
|
||||
*ideal_cursor_column = 0;
|
||||
*command_message = "".to_string();
|
||||
Ok(command_message.clone())
|
||||
}
|
||||
"move_line_end" => {
|
||||
let current_input = state.get_current_input();
|
||||
let new_pos = current_input.len(); // Allow cursor at end
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
*command_message = "".to_string();
|
||||
Ok(command_message.clone())
|
||||
}
|
||||
// Actions handled by main event loop (mode changes)
|
||||
"enter_edit_mode_before" | "enter_edit_mode_after"
|
||||
| "enter_command_mode" | "exit_highlight_mode" => {
|
||||
key_sequence_tracker.reset();
|
||||
// These actions are primarily mode changes handled by the main event loop.
|
||||
// The message here might be overridden by the main loop's message for mode change.
|
||||
*command_message = "Mode change initiated".to_string();
|
||||
Ok(command_message.clone())
|
||||
}
|
||||
_ => {
|
||||
key_sequence_tracker.reset();
|
||||
*command_message =
|
||||
format!("Unknown read-only action: {}", action);
|
||||
Ok(command_message.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,343 +0,0 @@
|
||||
// src/functions/modes/read_only/auth_ro.rs
|
||||
|
||||
use crate::config::binds::key_sequences::KeySequenceTracker;
|
||||
use crate::state::app::state::AppState;
|
||||
use canvas::canvas::CanvasState;
|
||||
use anyhow::Result;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum CharType {
|
||||
Whitespace,
|
||||
Alphanumeric,
|
||||
Punctuation,
|
||||
}
|
||||
|
||||
pub async fn execute_action<S: CanvasState>(
|
||||
action: &str,
|
||||
app_state: &mut AppState,
|
||||
state: &mut S,
|
||||
ideal_cursor_column: &mut usize,
|
||||
key_sequence_tracker: &mut KeySequenceTracker,
|
||||
command_message: &mut String,
|
||||
) -> Result<String> {
|
||||
match action {
|
||||
"previous_entry" | "next_entry" => {
|
||||
key_sequence_tracker.reset();
|
||||
Ok(format!(
|
||||
"Action '{}' should be handled by context-specific logic",
|
||||
action
|
||||
))
|
||||
}
|
||||
"move_up" => {
|
||||
key_sequence_tracker.reset();
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields == 0 {
|
||||
return Ok("No fields to navigate.".to_string());
|
||||
}
|
||||
let current_field = state.current_field();
|
||||
let new_field = current_field.saturating_sub(1);
|
||||
state.set_current_field(new_field);
|
||||
let current_input = state.get_current_input();
|
||||
let max_cursor_pos = if current_input.is_empty() {
|
||||
0
|
||||
} else {
|
||||
current_input.len().saturating_sub(1)
|
||||
};
|
||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
|
||||
Ok("move up from functions/modes/read_only/auth_ro.rs".to_string())
|
||||
}
|
||||
"move_down" => {
|
||||
key_sequence_tracker.reset();
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields == 0 {
|
||||
return Ok("No fields to navigate.".to_string());
|
||||
}
|
||||
let current_field = state.current_field();
|
||||
let last_field_index = num_fields - 1;
|
||||
|
||||
if current_field == last_field_index {
|
||||
// Already on the last field, move focus outside
|
||||
app_state.ui.focus_outside_canvas = true;
|
||||
app_state.focused_button_index= 0;
|
||||
key_sequence_tracker.reset();
|
||||
Ok("Focus moved below canvas".to_string())
|
||||
} else {
|
||||
// Move to the next field within the canvas
|
||||
let new_field = (current_field + 1).min(last_field_index);
|
||||
state.set_current_field(new_field);
|
||||
let current_input = state.get_current_input();
|
||||
let max_cursor_pos = if current_input.is_empty() {
|
||||
0
|
||||
} else {
|
||||
current_input.len().saturating_sub(1)
|
||||
};
|
||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
Ok("".to_string()) // Clear previous debug message
|
||||
}
|
||||
}
|
||||
"move_first_line" => {
|
||||
key_sequence_tracker.reset();
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields == 0 {
|
||||
return Ok("No fields to navigate to.".to_string());
|
||||
}
|
||||
state.set_current_field(0);
|
||||
let current_input = state.get_current_input();
|
||||
let max_cursor_pos = if current_input.is_empty() {
|
||||
0
|
||||
} else {
|
||||
current_input.len().saturating_sub(1)
|
||||
};
|
||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_last_line" => {
|
||||
key_sequence_tracker.reset();
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields == 0 {
|
||||
return Ok("No fields to navigate to.".to_string());
|
||||
}
|
||||
let last_field_index = num_fields - 1;
|
||||
state.set_current_field(last_field_index);
|
||||
let current_input = state.get_current_input();
|
||||
let max_cursor_pos = if current_input.is_empty() {
|
||||
0
|
||||
} else {
|
||||
current_input.len().saturating_sub(1)
|
||||
};
|
||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
Ok("".to_string())
|
||||
}
|
||||
"exit_edit_mode" => {
|
||||
key_sequence_tracker.reset();
|
||||
command_message.clear();
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_left" => {
|
||||
let current_pos = state.current_cursor_pos();
|
||||
let new_pos = current_pos.saturating_sub(1);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_right" => {
|
||||
let current_input = state.get_current_input();
|
||||
let current_pos = state.current_cursor_pos();
|
||||
if !current_input.is_empty()
|
||||
&& current_pos < current_input.len().saturating_sub(1)
|
||||
{
|
||||
let new_pos = current_pos + 1;
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_word_next" => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let new_pos =
|
||||
find_next_word_start(current_input, state.current_cursor_pos());
|
||||
let final_pos = new_pos.min(current_input.len().saturating_sub(1));
|
||||
state.set_current_cursor_pos(final_pos);
|
||||
*ideal_cursor_column = final_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_word_end" => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let current_pos = state.current_cursor_pos();
|
||||
let new_pos = find_word_end(current_input, current_pos);
|
||||
|
||||
let final_pos = if new_pos != current_pos {
|
||||
new_pos
|
||||
} else {
|
||||
find_word_end(current_input, new_pos + 1)
|
||||
};
|
||||
|
||||
let max_valid_index = current_input.len().saturating_sub(1);
|
||||
let clamped_pos = final_pos.min(max_valid_index);
|
||||
state.set_current_cursor_pos(clamped_pos);
|
||||
*ideal_cursor_column = clamped_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_word_prev" => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let new_pos = find_prev_word_start(
|
||||
current_input,
|
||||
state.current_cursor_pos(),
|
||||
);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_word_end_prev" => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let new_pos = find_prev_word_end(
|
||||
current_input,
|
||||
state.current_cursor_pos(),
|
||||
);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
Ok("Moved to previous word end".to_string())
|
||||
}
|
||||
"move_line_start" => {
|
||||
state.set_current_cursor_pos(0);
|
||||
*ideal_cursor_column = 0;
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_line_end" => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let new_pos = current_input.len().saturating_sub(1);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
} else {
|
||||
state.set_current_cursor_pos(0);
|
||||
*ideal_cursor_column = 0;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
_ => {
|
||||
key_sequence_tracker.reset();
|
||||
Ok(format!("Unknown read-only action: {}", action))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn get_char_type(c: char) -> CharType {
|
||||
if c.is_whitespace() {
|
||||
CharType::Whitespace
|
||||
} else if c.is_alphanumeric() {
|
||||
CharType::Alphanumeric
|
||||
} else {
|
||||
CharType::Punctuation
|
||||
}
|
||||
}
|
||||
|
||||
fn find_next_word_start(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
if chars.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
let current_pos = current_pos.min(chars.len());
|
||||
|
||||
if current_pos == chars.len() {
|
||||
return current_pos;
|
||||
}
|
||||
|
||||
let mut pos = current_pos;
|
||||
let initial_type = get_char_type(chars[pos]);
|
||||
|
||||
while pos < chars.len() && get_char_type(chars[pos]) == initial_type {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
while pos < chars.len() && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
pos
|
||||
}
|
||||
|
||||
fn find_word_end(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut pos = current_pos.min(len - 1);
|
||||
let current_type = get_char_type(chars[pos]);
|
||||
if current_type != CharType::Whitespace {
|
||||
while pos < len && get_char_type(chars[pos]) == current_type {
|
||||
pos += 1;
|
||||
}
|
||||
return pos.saturating_sub(1);
|
||||
}
|
||||
|
||||
pos = find_next_word_start(text, pos);
|
||||
if pos >= len {
|
||||
return len.saturating_sub(1);
|
||||
}
|
||||
|
||||
let word_type = get_char_type(chars[pos]);
|
||||
while pos < len && get_char_type(chars[pos]) == word_type {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
pos.saturating_sub(1).min(len.saturating_sub(1))
|
||||
}
|
||||
|
||||
fn find_prev_word_start(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
if chars.is_empty() || current_pos == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut pos = current_pos.saturating_sub(1);
|
||||
|
||||
while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
if get_char_type(chars[pos]) != CharType::Whitespace {
|
||||
let word_type = get_char_type(chars[pos]);
|
||||
while pos > 0 && get_char_type(chars[pos - 1]) == word_type {
|
||||
pos -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
if pos == 0 && get_char_type(chars[0]) == CharType::Whitespace {
|
||||
0
|
||||
} else {
|
||||
pos
|
||||
}
|
||||
}
|
||||
|
||||
fn find_prev_word_end(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
if chars.is_empty() || current_pos == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut pos = current_pos.saturating_sub(1);
|
||||
|
||||
while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
if pos == 0 && get_char_type(chars[0]) == CharType::Whitespace {
|
||||
return 0;
|
||||
}
|
||||
if pos == 0 && get_char_type(chars[0]) != CharType::Whitespace {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let word_type = get_char_type(chars[pos]);
|
||||
while pos > 0 && get_char_type(chars[pos - 1]) == word_type {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
while pos > 0 && get_char_type(chars[pos - 1]) == CharType::Whitespace {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
if pos > 0 {
|
||||
pos - 1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
@@ -1,329 +0,0 @@
|
||||
// src/functions/modes/read_only/form_ro.rs
|
||||
|
||||
use crate::config::binds::key_sequences::KeySequenceTracker;
|
||||
use canvas::canvas::CanvasState;
|
||||
use anyhow::Result;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum CharType {
|
||||
Whitespace,
|
||||
Alphanumeric,
|
||||
Punctuation,
|
||||
}
|
||||
|
||||
pub async fn execute_action<S: CanvasState>(
|
||||
action: &str,
|
||||
state: &mut S,
|
||||
ideal_cursor_column: &mut usize,
|
||||
key_sequence_tracker: &mut KeySequenceTracker,
|
||||
command_message: &mut String,
|
||||
) -> Result<String> {
|
||||
match action {
|
||||
"previous_entry" | "next_entry" => {
|
||||
key_sequence_tracker.reset();
|
||||
Ok(format!(
|
||||
"Action '{}' should be handled by context-specific logic",
|
||||
action
|
||||
))
|
||||
}
|
||||
"move_up" => {
|
||||
key_sequence_tracker.reset();
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields == 0 {
|
||||
return Ok("No fields to navigate.".to_string());
|
||||
}
|
||||
let current_field = state.current_field();
|
||||
let new_field = current_field.saturating_sub(1);
|
||||
state.set_current_field(new_field);
|
||||
let current_input = state.get_current_input();
|
||||
let max_cursor_pos = if current_input.is_empty() {
|
||||
0
|
||||
} else {
|
||||
current_input.len().saturating_sub(1)
|
||||
};
|
||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_down" => {
|
||||
key_sequence_tracker.reset();
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields == 0 {
|
||||
return Ok("No fields to navigate.".to_string());
|
||||
}
|
||||
let current_field = state.current_field();
|
||||
let new_field = (current_field + 1).min(num_fields - 1);
|
||||
state.set_current_field(new_field);
|
||||
let current_input = state.get_current_input();
|
||||
let max_cursor_pos = if current_input.is_empty() {
|
||||
0
|
||||
} else {
|
||||
current_input.len().saturating_sub(1)
|
||||
};
|
||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_first_line" => {
|
||||
key_sequence_tracker.reset();
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields == 0 {
|
||||
return Ok("No fields to navigate to.".to_string());
|
||||
}
|
||||
state.set_current_field(0);
|
||||
let current_input = state.get_current_input();
|
||||
let max_cursor_pos = if current_input.is_empty() {
|
||||
0
|
||||
} else {
|
||||
current_input.len().saturating_sub(1)
|
||||
};
|
||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_last_line" => {
|
||||
key_sequence_tracker.reset();
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields == 0 {
|
||||
return Ok("No fields to navigate to.".to_string());
|
||||
}
|
||||
let last_field_index = num_fields - 1;
|
||||
state.set_current_field(last_field_index);
|
||||
let current_input = state.get_current_input();
|
||||
let max_cursor_pos = if current_input.is_empty() {
|
||||
0
|
||||
} else {
|
||||
current_input.len().saturating_sub(1)
|
||||
};
|
||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
Ok("".to_string())
|
||||
}
|
||||
"exit_edit_mode" => {
|
||||
key_sequence_tracker.reset();
|
||||
command_message.clear();
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_left" => {
|
||||
let current_pos = state.current_cursor_pos();
|
||||
let new_pos = current_pos.saturating_sub(1);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_right" => {
|
||||
let current_input = state.get_current_input();
|
||||
let current_pos = state.current_cursor_pos();
|
||||
if !current_input.is_empty()
|
||||
&& current_pos < current_input.len().saturating_sub(1)
|
||||
{
|
||||
let new_pos = current_pos + 1;
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_word_next" => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let new_pos =
|
||||
find_next_word_start(current_input, state.current_cursor_pos());
|
||||
let final_pos = new_pos.min(current_input.len().saturating_sub(1));
|
||||
state.set_current_cursor_pos(final_pos);
|
||||
*ideal_cursor_column = final_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_word_end" => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let current_pos = state.current_cursor_pos();
|
||||
let new_pos = find_word_end(current_input, current_pos);
|
||||
|
||||
let final_pos = if new_pos != current_pos {
|
||||
new_pos
|
||||
} else {
|
||||
find_word_end(current_input, new_pos + 1)
|
||||
};
|
||||
|
||||
let max_valid_index = current_input.len().saturating_sub(1);
|
||||
let clamped_pos = final_pos.min(max_valid_index);
|
||||
state.set_current_cursor_pos(clamped_pos);
|
||||
*ideal_cursor_column = clamped_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_word_prev" => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let new_pos = find_prev_word_start(
|
||||
current_input,
|
||||
state.current_cursor_pos(),
|
||||
);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_word_end_prev" => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let new_pos = find_prev_word_end(
|
||||
current_input,
|
||||
state.current_cursor_pos(),
|
||||
);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
Ok("Moved to previous word end".to_string())
|
||||
}
|
||||
"move_line_start" => {
|
||||
state.set_current_cursor_pos(0);
|
||||
*ideal_cursor_column = 0;
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_line_end" => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let new_pos = current_input.len().saturating_sub(1);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
} else {
|
||||
state.set_current_cursor_pos(0);
|
||||
*ideal_cursor_column = 0;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
_ => {
|
||||
key_sequence_tracker.reset();
|
||||
Ok(format!("Unknown read-only action: {}", action))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn get_char_type(c: char) -> CharType {
|
||||
if c.is_whitespace() {
|
||||
CharType::Whitespace
|
||||
} else if c.is_alphanumeric() {
|
||||
CharType::Alphanumeric
|
||||
} else {
|
||||
CharType::Punctuation
|
||||
}
|
||||
}
|
||||
|
||||
fn find_next_word_start(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
if chars.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
let current_pos = current_pos.min(chars.len());
|
||||
|
||||
if current_pos == chars.len() {
|
||||
return current_pos;
|
||||
}
|
||||
|
||||
let mut pos = current_pos;
|
||||
let initial_type = get_char_type(chars[pos]);
|
||||
|
||||
while pos < chars.len() && get_char_type(chars[pos]) == initial_type {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
while pos < chars.len() && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
pos
|
||||
}
|
||||
|
||||
fn find_word_end(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut pos = current_pos.min(len - 1);
|
||||
let current_type = get_char_type(chars[pos]);
|
||||
if current_type != CharType::Whitespace {
|
||||
while pos < len && get_char_type(chars[pos]) == current_type {
|
||||
pos += 1;
|
||||
}
|
||||
return pos.saturating_sub(1);
|
||||
}
|
||||
|
||||
pos = find_next_word_start(text, pos);
|
||||
if pos >= len {
|
||||
return len.saturating_sub(1);
|
||||
}
|
||||
|
||||
let word_type = get_char_type(chars[pos]);
|
||||
while pos < len && get_char_type(chars[pos]) == word_type {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
pos.saturating_sub(1).min(len.saturating_sub(1))
|
||||
}
|
||||
|
||||
fn find_prev_word_start(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
if chars.is_empty() || current_pos == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut pos = current_pos.saturating_sub(1);
|
||||
|
||||
while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
if get_char_type(chars[pos]) != CharType::Whitespace {
|
||||
let word_type = get_char_type(chars[pos]);
|
||||
while pos > 0 && get_char_type(chars[pos - 1]) == word_type {
|
||||
pos -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
if pos == 0 && get_char_type(chars[0]) == CharType::Whitespace {
|
||||
0
|
||||
} else {
|
||||
pos
|
||||
}
|
||||
}
|
||||
|
||||
fn find_prev_word_end(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
if chars.is_empty() || current_pos == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut pos = current_pos.saturating_sub(1);
|
||||
|
||||
while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
if pos == 0 && get_char_type(chars[0]) == CharType::Whitespace {
|
||||
return 0;
|
||||
}
|
||||
if pos == 0 && get_char_type(chars[0]) != CharType::Whitespace {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let word_type = get_char_type(chars[pos]);
|
||||
while pos > 0 && get_char_type(chars[pos - 1]) == word_type {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
while pos > 0 && get_char_type(chars[pos - 1]) == CharType::Whitespace {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
if pos > 0 {
|
||||
pos - 1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
// src/modes/canvas/edit.rs
|
||||
use crate::config::binds::config::Config;
|
||||
use crate::functions::modes::edit::{
|
||||
add_logic_e, add_table_e, form_e,
|
||||
};
|
||||
use crate::modes::handlers::event::EventHandler;
|
||||
use crate::services::grpc_client::GrpcClient;
|
||||
use crate::state::app::state::AppState;
|
||||
@@ -127,6 +124,23 @@ pub async fn handle_form_edit_with_canvas(
|
||||
Ok(String::new())
|
||||
}
|
||||
|
||||
/// Helper function to execute a specific action using canvas library
|
||||
async fn execute_canvas_action(
|
||||
action: &str,
|
||||
key: KeyEvent,
|
||||
form_state: &mut FormState,
|
||||
ideal_cursor_column: &mut usize,
|
||||
) -> Result<String> {
|
||||
let canvas_action = CanvasAction::from_string(action);
|
||||
match ActionDispatcher::dispatch(canvas_action, form_state, ideal_cursor_column).await {
|
||||
Ok(ActionResult::Success(msg)) => Ok(msg.unwrap_or_default()),
|
||||
Ok(ActionResult::HandledByFeature(msg)) => Ok(msg),
|
||||
Ok(ActionResult::Error(msg)) => Ok(format!("Error: {}", msg)),
|
||||
Ok(ActionResult::RequiresContext(msg)) => Ok(format!("Context needed: {}", msg)),
|
||||
Err(e) => Ok(format!("Action failed: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
async fn handle_canvas_state_edit<S: CanvasState>(
|
||||
@@ -291,8 +305,8 @@ pub async fn handle_edit_event(
|
||||
} else {
|
||||
"insert_char"
|
||||
};
|
||||
// FIX: Pass &mut event_handler.ideal_cursor_column
|
||||
form_e::execute_edit_action(
|
||||
// FIXED: Use canvas library instead of form_e::execute_edit_action
|
||||
execute_canvas_action(
|
||||
action,
|
||||
key,
|
||||
form_state,
|
||||
@@ -321,8 +335,8 @@ pub async fn handle_edit_event(
|
||||
{
|
||||
// Handle Enter key (next field)
|
||||
if action_str == "enter_decider" {
|
||||
// FIX: Pass &mut event_handler.ideal_cursor_column
|
||||
let msg = form_e::execute_edit_action(
|
||||
// FIXED: Use canvas library instead of form_e::execute_edit_action
|
||||
let msg = execute_canvas_action(
|
||||
"next_field",
|
||||
key,
|
||||
form_state,
|
||||
@@ -375,8 +389,8 @@ pub async fn handle_edit_event(
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
// FIX: Pass &mut event_handler.ideal_cursor_column
|
||||
form_e::execute_edit_action(
|
||||
// FIXED: Use canvas library instead of form_e::execute_edit_action
|
||||
execute_canvas_action(
|
||||
action_str,
|
||||
key,
|
||||
form_state,
|
||||
@@ -426,8 +440,8 @@ pub async fn handle_edit_event(
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
// FIX: Pass &mut event_handler.ideal_cursor_column
|
||||
form_e::execute_edit_action(
|
||||
// FIXED: Use canvas library instead of form_e::execute_edit_action
|
||||
execute_canvas_action(
|
||||
"insert_char",
|
||||
key,
|
||||
form_state,
|
||||
|
||||
@@ -5,16 +5,85 @@ use crate::config::binds::key_sequences::KeySequenceTracker;
|
||||
use crate::services::grpc_client::GrpcClient;
|
||||
use crate::state::pages::auth::LoginState;
|
||||
use crate::state::pages::auth::RegisterState;
|
||||
use crate::state::pages::canvas_state::CanvasState as LocalCanvasState;
|
||||
use crate::state::pages::form::FormState;
|
||||
use crate::state::pages::add_logic::AddLogicState;
|
||||
use crate::state::pages::add_table::AddTableState;
|
||||
use crate::state::app::state::AppState;
|
||||
use crate::functions::modes::read_only::{add_logic_ro, auth_ro, form_ro, add_table_ro};
|
||||
use canvas::{canvas::{CanvasAction, CanvasState, ActionResult}, dispatcher::ActionDispatcher};
|
||||
use crossterm::event::KeyEvent;
|
||||
use anyhow::Result;
|
||||
|
||||
/// Helper function to dispatch canvas action for any CanvasState
|
||||
async fn dispatch_canvas_action<S: CanvasState>(
|
||||
action: &str,
|
||||
state: &mut S,
|
||||
ideal_cursor_column: &mut usize,
|
||||
) -> String {
|
||||
let canvas_action = CanvasAction::from_string(action);
|
||||
match ActionDispatcher::dispatch(canvas_action, state, ideal_cursor_column).await {
|
||||
Ok(ActionResult::Success(msg)) => msg.unwrap_or_default(),
|
||||
Ok(ActionResult::HandledByFeature(msg)) => msg,
|
||||
Ok(ActionResult::Error(msg)) => format!("Error: {}", msg),
|
||||
Ok(ActionResult::RequiresContext(msg)) => format!("Context needed: {}", msg),
|
||||
Err(e) => format!("Action failed: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to dispatch canvas action to the appropriate state based on UI
|
||||
async fn dispatch_to_active_state(
|
||||
action: &str,
|
||||
app_state: &AppState,
|
||||
form_state: &mut FormState,
|
||||
login_state: &mut LoginState,
|
||||
register_state: &mut RegisterState,
|
||||
add_table_state: &mut AddTableState,
|
||||
add_logic_state: &mut AddLogicState,
|
||||
ideal_cursor_column: &mut usize,
|
||||
) -> String {
|
||||
if app_state.ui.show_add_table {
|
||||
dispatch_canvas_action(action, add_table_state, ideal_cursor_column).await
|
||||
} else if app_state.ui.show_add_logic {
|
||||
dispatch_canvas_action(action, add_logic_state, ideal_cursor_column).await
|
||||
} else if app_state.ui.show_register {
|
||||
dispatch_canvas_action(action, register_state, ideal_cursor_column).await
|
||||
} else if app_state.ui.show_login {
|
||||
dispatch_canvas_action(action, login_state, ideal_cursor_column).await
|
||||
} else {
|
||||
dispatch_canvas_action(action, form_state, ideal_cursor_column).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to handle context-specific actions that need special treatment
|
||||
async fn handle_context_action(
|
||||
action: &str,
|
||||
app_state: &AppState,
|
||||
form_state: &mut FormState,
|
||||
grpc_client: &mut GrpcClient,
|
||||
ideal_cursor_column: &mut usize,
|
||||
) -> Result<Option<String>> {
|
||||
const CONTEXT_ACTIONS_FORM: &[&str] = &[
|
||||
"previous_entry",
|
||||
"next_entry",
|
||||
];
|
||||
const CONTEXT_ACTIONS_LOGIN: &[&str] = &[
|
||||
"previous_entry",
|
||||
"next_entry",
|
||||
];
|
||||
|
||||
if app_state.ui.show_form && CONTEXT_ACTIONS_FORM.contains(&action) {
|
||||
Ok(Some(crate::tui::functions::form::handle_action(
|
||||
action,
|
||||
form_state,
|
||||
grpc_client,
|
||||
ideal_cursor_column,
|
||||
).await?))
|
||||
} else if app_state.ui.show_login && CONTEXT_ACTIONS_LOGIN.contains(&action) {
|
||||
Ok(Some(crate::tui::functions::login::handle_action(action).await?))
|
||||
} else {
|
||||
Ok(None) // Not a context action, use regular canvas dispatch
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_form_readonly_with_canvas(
|
||||
key_event: KeyEvent,
|
||||
config: &Config,
|
||||
@@ -89,8 +158,7 @@ pub async fn handle_read_only_event(
|
||||
}
|
||||
|
||||
if config.is_enter_edit_mode_after(key.code, key.modifiers) {
|
||||
// Determine target state to adjust cursor
|
||||
|
||||
// Determine target state to adjust cursor - all states now use CanvasState trait
|
||||
if app_state.ui.show_login {
|
||||
let current_input = login_state.get_current_input();
|
||||
let current_pos = login_state.current_cursor_pos();
|
||||
@@ -120,8 +188,7 @@ pub async fn handle_read_only_event(
|
||||
*ideal_cursor_column = add_table_state.current_cursor_pos();
|
||||
}
|
||||
} else {
|
||||
// Handle FormState (uses library CanvasState)
|
||||
use canvas::canvas::CanvasState as LibraryCanvasState; // Import at the top of the function
|
||||
// Handle FormState
|
||||
let current_input = form_state.get_current_input();
|
||||
let current_pos = form_state.current_cursor_pos();
|
||||
if !current_input.is_empty() && current_pos < current_input.len() {
|
||||
@@ -135,76 +202,31 @@ pub async fn handle_read_only_event(
|
||||
return Ok((false, command_message.clone()));
|
||||
}
|
||||
|
||||
const CONTEXT_ACTIONS_FORM: &[&str] = &[
|
||||
"previous_entry",
|
||||
"next_entry",
|
||||
];
|
||||
const CONTEXT_ACTIONS_LOGIN: &[&str] = &[
|
||||
"previous_entry",
|
||||
"next_entry",
|
||||
];
|
||||
|
||||
if key.modifiers.is_empty() {
|
||||
key_sequence_tracker.add_key(key.code);
|
||||
let sequence = key_sequence_tracker.get_sequence();
|
||||
|
||||
if let Some(action) = config.matches_key_sequence_generalized(&sequence).as_deref() {
|
||||
let result = if app_state.ui.show_form && CONTEXT_ACTIONS_FORM.contains(&action) {
|
||||
crate::tui::functions::form::handle_action(
|
||||
// Try context-specific actions first, otherwise use canvas dispatch
|
||||
let result = if let Some(context_result) = handle_context_action(
|
||||
action,
|
||||
app_state,
|
||||
form_state,
|
||||
grpc_client,
|
||||
ideal_cursor_column,
|
||||
).await? {
|
||||
context_result
|
||||
} else {
|
||||
dispatch_to_active_state(
|
||||
action,
|
||||
app_state,
|
||||
form_state,
|
||||
grpc_client,
|
||||
ideal_cursor_column,
|
||||
)
|
||||
.await?
|
||||
} else if app_state.ui.show_login && CONTEXT_ACTIONS_LOGIN.contains(&action) {
|
||||
crate::tui::functions::login::handle_action(action).await?
|
||||
} else if app_state.ui.show_add_table {
|
||||
add_table_ro::execute_action(
|
||||
action,
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
add_table_state,
|
||||
ideal_cursor_column,
|
||||
key_sequence_tracker,
|
||||
command_message,
|
||||
).await?
|
||||
} else if app_state.ui.show_add_logic {
|
||||
add_logic_ro::execute_action(
|
||||
action,
|
||||
app_state,
|
||||
add_logic_state,
|
||||
ideal_cursor_column,
|
||||
key_sequence_tracker,
|
||||
command_message,
|
||||
).await?
|
||||
} else if app_state.ui.show_register{
|
||||
auth_ro::execute_action(
|
||||
action,
|
||||
app_state,
|
||||
register_state,
|
||||
ideal_cursor_column,
|
||||
key_sequence_tracker,
|
||||
command_message,
|
||||
).await?
|
||||
} else if app_state.ui.show_login {
|
||||
auth_ro::execute_action(
|
||||
action,
|
||||
app_state,
|
||||
login_state,
|
||||
ideal_cursor_column,
|
||||
key_sequence_tracker,
|
||||
command_message,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
form_ro::execute_action(
|
||||
action,
|
||||
form_state,
|
||||
ideal_cursor_column,
|
||||
key_sequence_tracker,
|
||||
command_message,
|
||||
)
|
||||
.await?
|
||||
).await
|
||||
};
|
||||
key_sequence_tracker.reset();
|
||||
return Ok((false, result));
|
||||
@@ -216,62 +238,26 @@ pub async fn handle_read_only_event(
|
||||
|
||||
if sequence.len() == 1 && !config.is_key_sequence_prefix(&sequence) {
|
||||
if let Some(action) = config.get_read_only_action_for_key(key.code, key.modifiers).as_deref() {
|
||||
let result = if app_state.ui.show_form && CONTEXT_ACTIONS_FORM.contains(&action) {
|
||||
crate::tui::functions::form::handle_action(
|
||||
// Try context-specific actions first, otherwise use canvas dispatch
|
||||
let result = if let Some(context_result) = handle_context_action(
|
||||
action,
|
||||
app_state,
|
||||
form_state,
|
||||
grpc_client,
|
||||
ideal_cursor_column,
|
||||
).await? {
|
||||
context_result
|
||||
} else {
|
||||
dispatch_to_active_state(
|
||||
action,
|
||||
app_state,
|
||||
form_state,
|
||||
grpc_client,
|
||||
ideal_cursor_column,
|
||||
)
|
||||
.await?
|
||||
} else if app_state.ui.show_login && CONTEXT_ACTIONS_LOGIN.contains(&action) {
|
||||
crate::tui::functions::login::handle_action(action).await?
|
||||
} else if app_state.ui.show_add_table {
|
||||
add_table_ro::execute_action(
|
||||
action,
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
add_table_state,
|
||||
ideal_cursor_column,
|
||||
key_sequence_tracker,
|
||||
command_message,
|
||||
).await?
|
||||
} else if app_state.ui.show_add_logic {
|
||||
add_logic_ro::execute_action(
|
||||
action,
|
||||
app_state,
|
||||
add_logic_state,
|
||||
ideal_cursor_column,
|
||||
key_sequence_tracker,
|
||||
command_message,
|
||||
).await?
|
||||
} else if app_state.ui.show_register {
|
||||
auth_ro::execute_action(
|
||||
action,
|
||||
app_state,
|
||||
register_state,
|
||||
ideal_cursor_column,
|
||||
key_sequence_tracker,
|
||||
command_message,
|
||||
).await?
|
||||
} else if app_state.ui.show_login {
|
||||
auth_ro::execute_action(
|
||||
action,
|
||||
app_state,
|
||||
login_state,
|
||||
ideal_cursor_column,
|
||||
key_sequence_tracker,
|
||||
command_message,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
form_ro::execute_action(
|
||||
action,
|
||||
form_state,
|
||||
ideal_cursor_column,
|
||||
key_sequence_tracker,
|
||||
command_message,
|
||||
)
|
||||
.await?
|
||||
).await
|
||||
};
|
||||
key_sequence_tracker.reset();
|
||||
return Ok((false, result));
|
||||
@@ -282,62 +268,26 @@ pub async fn handle_read_only_event(
|
||||
key_sequence_tracker.reset();
|
||||
|
||||
if let Some(action) = config.get_read_only_action_for_key(key.code, key.modifiers).as_deref() {
|
||||
let result = if app_state.ui.show_form && CONTEXT_ACTIONS_FORM.contains(&action) {
|
||||
crate::tui::functions::form::handle_action(
|
||||
// Try context-specific actions first, otherwise use canvas dispatch
|
||||
let result = if let Some(context_result) = handle_context_action(
|
||||
action,
|
||||
app_state,
|
||||
form_state,
|
||||
grpc_client,
|
||||
ideal_cursor_column,
|
||||
).await? {
|
||||
context_result
|
||||
} else {
|
||||
dispatch_to_active_state(
|
||||
action,
|
||||
app_state,
|
||||
form_state,
|
||||
grpc_client,
|
||||
ideal_cursor_column,
|
||||
)
|
||||
.await?
|
||||
} else if app_state.ui.show_login && CONTEXT_ACTIONS_LOGIN.contains(&action) {
|
||||
crate::tui::functions::login::handle_action(action).await?
|
||||
} else if app_state.ui.show_add_table {
|
||||
add_table_ro::execute_action(
|
||||
action,
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
add_table_state,
|
||||
ideal_cursor_column,
|
||||
key_sequence_tracker,
|
||||
command_message,
|
||||
).await?
|
||||
} else if app_state.ui.show_add_logic {
|
||||
add_logic_ro::execute_action(
|
||||
action,
|
||||
app_state,
|
||||
add_logic_state,
|
||||
ideal_cursor_column,
|
||||
key_sequence_tracker,
|
||||
command_message,
|
||||
).await?
|
||||
} else if app_state.ui.show_register {
|
||||
auth_ro::execute_action(
|
||||
action,
|
||||
app_state,
|
||||
register_state,
|
||||
ideal_cursor_column,
|
||||
key_sequence_tracker,
|
||||
command_message,
|
||||
).await?
|
||||
} else if app_state.ui.show_login {
|
||||
auth_ro::execute_action(
|
||||
action,
|
||||
app_state,
|
||||
login_state,
|
||||
ideal_cursor_column,
|
||||
key_sequence_tracker,
|
||||
command_message,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
form_ro::execute_action(
|
||||
action,
|
||||
form_state,
|
||||
ideal_cursor_column,
|
||||
key_sequence_tracker,
|
||||
command_message,
|
||||
)
|
||||
.await?
|
||||
).await
|
||||
};
|
||||
return Ok((false, result));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// src/modes/handlers.rs
|
||||
pub mod event;
|
||||
pub mod event_helper;
|
||||
pub mod mode_manager;
|
||||
|
||||
@@ -15,23 +15,20 @@ use crate::modes::{
|
||||
general::{dialog, navigation},
|
||||
handlers::mode_manager::{AppMode, ModeManager},
|
||||
};
|
||||
use crate::state::pages::canvas_state::CanvasState as LegacyCanvasState;
|
||||
use crate::services::auth::AuthClient;
|
||||
use crate::services::grpc_client::GrpcClient;
|
||||
use canvas::{canvas::CanvasAction, dispatcher::ActionDispatcher};
|
||||
use canvas::canvas::CanvasState as LibraryCanvasState;
|
||||
use super::event_helper::*;
|
||||
use canvas::canvas::CanvasState; // Only need this import now
|
||||
use crate::state::{
|
||||
app::{
|
||||
buffer::{AppView, BufferState},
|
||||
highlight::HighlightState,
|
||||
search::SearchState, // Correctly imported
|
||||
search::SearchState,
|
||||
state::AppState,
|
||||
},
|
||||
pages::{
|
||||
admin::AdminState,
|
||||
auth::{AuthState, LoginState, RegisterState},
|
||||
canvas_state::CanvasState,
|
||||
form::FormState,
|
||||
intro::IntroState,
|
||||
},
|
||||
@@ -89,7 +86,6 @@ pub struct EventHandler {
|
||||
pub navigation_state: NavigationState,
|
||||
pub search_result_sender: mpsc::UnboundedSender<Vec<Hit>>,
|
||||
pub search_result_receiver: mpsc::UnboundedReceiver<Vec<Hit>>,
|
||||
// --- ADDED FOR LIVE AUTOCOMPLETE ---
|
||||
pub autocomplete_result_sender: mpsc::UnboundedSender<Vec<Hit>>,
|
||||
pub autocomplete_result_receiver: mpsc::UnboundedReceiver<Vec<Hit>>,
|
||||
}
|
||||
@@ -103,7 +99,7 @@ impl EventHandler {
|
||||
grpc_client: GrpcClient,
|
||||
) -> Result<Self> {
|
||||
let (search_tx, search_rx) = unbounded_channel();
|
||||
let (autocomplete_tx, autocomplete_rx) = unbounded_channel(); // ADDED
|
||||
let (autocomplete_tx, autocomplete_rx) = unbounded_channel();
|
||||
Ok(EventHandler {
|
||||
command_mode: false,
|
||||
command_input: String::new(),
|
||||
@@ -122,7 +118,6 @@ impl EventHandler {
|
||||
navigation_state: NavigationState::new(),
|
||||
search_result_sender: search_tx,
|
||||
search_result_receiver: search_rx,
|
||||
// --- ADDED ---
|
||||
autocomplete_result_sender: autocomplete_tx,
|
||||
autocomplete_result_receiver: autocomplete_rx,
|
||||
})
|
||||
@@ -136,6 +131,95 @@ impl EventHandler {
|
||||
self.navigation_state.activate_find_file(options);
|
||||
}
|
||||
|
||||
// Helper functions - replace the removed event_helper functions
|
||||
fn get_current_field_for_state(
|
||||
app_state: &AppState,
|
||||
login_state: &LoginState,
|
||||
register_state: &RegisterState,
|
||||
form_state: &FormState,
|
||||
) -> usize {
|
||||
if app_state.ui.show_login {
|
||||
login_state.current_field()
|
||||
} else if app_state.ui.show_register {
|
||||
register_state.current_field()
|
||||
} else {
|
||||
form_state.current_field()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_cursor_pos_for_state(
|
||||
app_state: &AppState,
|
||||
login_state: &LoginState,
|
||||
register_state: &RegisterState,
|
||||
form_state: &FormState,
|
||||
) -> usize {
|
||||
if app_state.ui.show_login {
|
||||
login_state.current_cursor_pos()
|
||||
} else if app_state.ui.show_register {
|
||||
register_state.current_cursor_pos()
|
||||
} else {
|
||||
form_state.current_cursor_pos()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_has_unsaved_changes_for_state(
|
||||
app_state: &AppState,
|
||||
login_state: &LoginState,
|
||||
register_state: &RegisterState,
|
||||
form_state: &FormState,
|
||||
) -> bool {
|
||||
if app_state.ui.show_login {
|
||||
login_state.has_unsaved_changes()
|
||||
} else if app_state.ui.show_register {
|
||||
register_state.has_unsaved_changes()
|
||||
} else {
|
||||
form_state.has_unsaved_changes()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_input_for_state<'a>(
|
||||
app_state: &AppState,
|
||||
login_state: &'a LoginState,
|
||||
register_state: &'a RegisterState,
|
||||
form_state: &'a FormState,
|
||||
) -> &'a str {
|
||||
if app_state.ui.show_login {
|
||||
login_state.get_current_input()
|
||||
} else if app_state.ui.show_register {
|
||||
register_state.get_current_input()
|
||||
} else {
|
||||
form_state.get_current_input()
|
||||
}
|
||||
}
|
||||
|
||||
fn set_current_cursor_pos_for_state(
|
||||
app_state: &AppState,
|
||||
login_state: &mut LoginState,
|
||||
register_state: &mut RegisterState,
|
||||
form_state: &mut FormState,
|
||||
pos: usize,
|
||||
) {
|
||||
if app_state.ui.show_login {
|
||||
login_state.set_current_cursor_pos(pos);
|
||||
} else if app_state.ui.show_register {
|
||||
register_state.set_current_cursor_pos(pos);
|
||||
} else {
|
||||
form_state.set_current_cursor_pos(pos);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_cursor_pos_for_mixed_state(
|
||||
app_state: &AppState,
|
||||
login_state: &LoginState,
|
||||
form_state: &FormState,
|
||||
) -> usize {
|
||||
if app_state.ui.show_login || app_state.ui.show_register {
|
||||
login_state.current_cursor_pos()
|
||||
} else {
|
||||
form_state.current_cursor_pos()
|
||||
}
|
||||
}
|
||||
|
||||
// This function handles state changes.
|
||||
async fn handle_search_palette_event(
|
||||
&mut self,
|
||||
@@ -199,7 +283,6 @@ impl EventHandler {
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// --- START CORRECTED LOGIC ---
|
||||
if trigger_search {
|
||||
search_state.is_loading = true;
|
||||
search_state.results.clear();
|
||||
@@ -214,7 +297,6 @@ impl EventHandler {
|
||||
"--- 1. Spawning search task for query: '{}' ---",
|
||||
query
|
||||
);
|
||||
// We now move the grpc_client into the task, just like with login.
|
||||
tokio::spawn(async move {
|
||||
info!("--- 2. Background task started. ---");
|
||||
match grpc_client.search_table(table_name, query).await {
|
||||
@@ -226,7 +308,6 @@ impl EventHandler {
|
||||
let _ = sender.send(response.hits);
|
||||
}
|
||||
Err(e) => {
|
||||
// THE FIX: Use the debug formatter `{:?}` to print the full error chain.
|
||||
error!("--- 3b. gRPC call failed: {:?} ---", e);
|
||||
let _ = sender.send(vec![]);
|
||||
}
|
||||
@@ -235,8 +316,6 @@ impl EventHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// The borrow on `app_state.search_state` ends here.
|
||||
// Now we can safely modify the Option itself.
|
||||
if should_close {
|
||||
app_state.search_state = None;
|
||||
app_state.ui.show_search_palette = false;
|
||||
@@ -264,7 +343,6 @@ impl EventHandler {
|
||||
) -> Result<EventOutcome> {
|
||||
if app_state.ui.show_search_palette {
|
||||
if let Event::Key(key_event) = event {
|
||||
// The call no longer passes grpc_client
|
||||
return self
|
||||
.handle_search_palette_event(
|
||||
key_event,
|
||||
@@ -581,7 +659,7 @@ impl EventHandler {
|
||||
if config.get_read_only_action_for_key(key_code, modifiers) == Some("enter_highlight_mode_linewise")
|
||||
&& ModeManager::can_enter_highlight_mode(current_mode)
|
||||
{
|
||||
let current_field_index = get_current_field_for_state(
|
||||
let current_field_index = Self::get_current_field_for_state(
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
@@ -596,13 +674,13 @@ impl EventHandler {
|
||||
else if config.get_read_only_action_for_key(key_code, modifiers) == Some("enter_highlight_mode")
|
||||
&& ModeManager::can_enter_highlight_mode(current_mode)
|
||||
{
|
||||
let current_field_index = get_current_field_for_state(
|
||||
let current_field_index = Self::get_current_field_for_state(
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
form_state
|
||||
);
|
||||
let current_cursor_pos = get_current_cursor_pos_for_state(
|
||||
let current_cursor_pos = Self::get_current_cursor_pos_for_state(
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
@@ -627,13 +705,13 @@ impl EventHandler {
|
||||
else if config.get_read_only_action_for_key(key_code, modifiers).as_deref() == Some("enter_edit_mode_after")
|
||||
&& ModeManager::can_enter_edit_mode(current_mode)
|
||||
{
|
||||
let current_input = get_current_input_for_state(
|
||||
let current_input = Self::get_current_input_for_state(
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
form_state
|
||||
);
|
||||
let current_cursor_pos = get_cursor_pos_for_mixed_state(
|
||||
let current_cursor_pos = Self::get_cursor_pos_for_mixed_state(
|
||||
app_state,
|
||||
login_state,
|
||||
form_state
|
||||
@@ -642,14 +720,14 @@ impl EventHandler {
|
||||
// Move cursor forward if possible
|
||||
if !current_input.is_empty() && current_cursor_pos < current_input.len() {
|
||||
let new_cursor_pos = current_cursor_pos + 1;
|
||||
set_current_cursor_pos_for_state(
|
||||
Self::set_current_cursor_pos_for_state(
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
form_state,
|
||||
new_cursor_pos
|
||||
);
|
||||
self.ideal_cursor_column = get_current_cursor_pos_for_state(
|
||||
self.ideal_cursor_column = Self::get_current_cursor_pos_for_state(
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
@@ -694,13 +772,13 @@ impl EventHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// Try canvas action for form first (NEW: Canvas library integration)
|
||||
// Try canvas action for form first
|
||||
if app_state.ui.show_form {
|
||||
if let Ok(Some(canvas_message)) = self.handle_form_canvas_action(
|
||||
key_event,
|
||||
config,
|
||||
form_state,
|
||||
false, // not edit mode
|
||||
false,
|
||||
).await {
|
||||
return Ok(EventOutcome::Ok(canvas_message));
|
||||
}
|
||||
@@ -753,7 +831,7 @@ impl EventHandler {
|
||||
&mut admin_state.add_table_state,
|
||||
&mut admin_state.add_logic_state,
|
||||
&mut self.key_sequence_tracker,
|
||||
&mut self.grpc_client, // <-- FIX 2
|
||||
&mut self.grpc_client,
|
||||
&mut self.command_message,
|
||||
&mut self.edit_mode_cooldown,
|
||||
&mut self.ideal_cursor_column,
|
||||
@@ -784,13 +862,13 @@ impl EventHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// Try canvas action for form first (NEW: Canvas library integration)
|
||||
// Try canvas action for form first
|
||||
if app_state.ui.show_form {
|
||||
if let Ok(Some(canvas_message)) = self.handle_form_canvas_action(
|
||||
key_event,
|
||||
config,
|
||||
form_state,
|
||||
true, // edit mode
|
||||
true,
|
||||
).await {
|
||||
if !canvas_message.is_empty() {
|
||||
self.command_message = canvas_message.clone();
|
||||
@@ -823,7 +901,7 @@ impl EventHandler {
|
||||
self.edit_mode_cooldown = true;
|
||||
|
||||
// Check for unsaved changes across all states
|
||||
let has_changes = get_has_unsaved_changes_for_state(
|
||||
let has_changes = Self::get_has_unsaved_changes_for_state(
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
@@ -840,13 +918,13 @@ impl EventHandler {
|
||||
terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?;
|
||||
|
||||
// Get current input and cursor position
|
||||
let current_input = get_current_input_for_state(
|
||||
let current_input = Self::get_current_input_for_state(
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
form_state
|
||||
);
|
||||
let current_cursor_pos = get_current_cursor_pos_for_state(
|
||||
let current_cursor_pos = Self::get_current_cursor_pos_for_state(
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
@@ -856,7 +934,7 @@ impl EventHandler {
|
||||
// Adjust cursor if it's beyond the input length
|
||||
if !current_input.is_empty() && current_cursor_pos >= current_input.len() {
|
||||
let new_pos = current_input.len() - 1;
|
||||
set_current_cursor_pos_for_state(
|
||||
Self::set_current_cursor_pos_for_state(
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
@@ -906,7 +984,7 @@ impl EventHandler {
|
||||
form_state,
|
||||
&mut self.command_input,
|
||||
&mut self.command_message,
|
||||
&mut self.grpc_client, // <-- FIX 5
|
||||
&mut self.grpc_client,
|
||||
command_handler,
|
||||
terminal,
|
||||
&mut current_position,
|
||||
@@ -1024,11 +1102,10 @@ impl EventHandler {
|
||||
async fn handle_form_canvas_action(
|
||||
&mut self,
|
||||
key_event: KeyEvent,
|
||||
_config: &Config, // Not used anymore - canvas has its own config
|
||||
_config: &Config,
|
||||
form_state: &mut FormState,
|
||||
is_edit_mode: bool,
|
||||
) -> Result<Option<String>> {
|
||||
// Load canvas config (canvas_config.toml or vim defaults)
|
||||
let canvas_config = canvas::config::CanvasConfig::load();
|
||||
|
||||
// Handle suggestion actions first if suggestions are active
|
||||
@@ -1083,7 +1160,6 @@ impl EventHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// FIXED: Use canvas config instead of client config
|
||||
let action_str = canvas_config.get_action_for_key(
|
||||
key_event.code,
|
||||
key_event.modifiers,
|
||||
@@ -1092,9 +1168,8 @@ impl EventHandler {
|
||||
);
|
||||
|
||||
if let Some(action_str) = action_str {
|
||||
// Filter out mode transition actions - let legacy handlers deal with these
|
||||
if Self::is_mode_transition_action(action_str) {
|
||||
return Ok(None); // Let legacy handler handle mode transitions
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let canvas_action = CanvasAction::from_string(&action_str);
|
||||
@@ -1131,7 +1206,6 @@ impl EventHandler {
|
||||
} else {
|
||||
// In read-only mode, only handle non-character keys
|
||||
let canvas_action = match key_event.code {
|
||||
// Only handle special keys that don't conflict with vim bindings
|
||||
KeyCode::Left => Some(CanvasAction::MoveLeft),
|
||||
KeyCode::Right => Some(CanvasAction::MoveRight),
|
||||
KeyCode::Up => Some(CanvasAction::MoveUp),
|
||||
@@ -1164,7 +1238,6 @@ impl EventHandler {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
// ADDED: Helper function to identify mode transition actions
|
||||
fn is_mode_transition_action(action: &str) -> bool {
|
||||
matches!(action,
|
||||
"exit" |
|
||||
@@ -1181,11 +1254,11 @@ impl EventHandler {
|
||||
"force_quit" |
|
||||
"save_and_quit" |
|
||||
"revert" |
|
||||
"enter_decider" | // This is also handled specially by legacy system
|
||||
"trigger_autocomplete" | // This is handled specially by legacy system
|
||||
"suggestion_up" | // These are handled above in suggestion logic
|
||||
"enter_decider" |
|
||||
"trigger_autocomplete" |
|
||||
"suggestion_up" |
|
||||
"suggestion_down" |
|
||||
"previous_entry" | // Navigation between records
|
||||
"previous_entry" |
|
||||
"next_entry" |
|
||||
"toggle_sidebar" |
|
||||
"toggle_buffer_list" |
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
|
||||
// src/modes/handlers/event_helper.rs
|
||||
//! Helper functions to handle the differences between legacy and library CanvasState traits
|
||||
|
||||
use crate::state::app::state::AppState;
|
||||
use crate::state::pages::{
|
||||
form::FormState,
|
||||
auth::{LoginState, RegisterState},
|
||||
};
|
||||
use crate::state::pages::canvas_state::CanvasState as LegacyCanvasState;
|
||||
use canvas::canvas::CanvasState as LibraryCanvasState;
|
||||
|
||||
/// Get the current field index from the appropriate state based on which UI is active
|
||||
pub fn get_current_field_for_state(
|
||||
app_state: &AppState,
|
||||
login_state: &LoginState,
|
||||
register_state: &RegisterState,
|
||||
form_state: &FormState,
|
||||
) -> usize {
|
||||
if app_state.ui.show_login {
|
||||
login_state.current_field() // Uses LegacyCanvasState
|
||||
} else if app_state.ui.show_register {
|
||||
register_state.current_field() // Uses LegacyCanvasState
|
||||
} else {
|
||||
form_state.current_field() // Uses LibraryCanvasState
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current cursor position from the appropriate state based on which UI is active
|
||||
pub fn get_current_cursor_pos_for_state(
|
||||
app_state: &AppState,
|
||||
login_state: &LoginState,
|
||||
register_state: &RegisterState,
|
||||
form_state: &FormState,
|
||||
) -> usize {
|
||||
if app_state.ui.show_login {
|
||||
login_state.current_cursor_pos() // Uses LegacyCanvasState
|
||||
} else if app_state.ui.show_register {
|
||||
register_state.current_cursor_pos() // Uses LegacyCanvasState
|
||||
} else {
|
||||
form_state.current_cursor_pos() // Uses LibraryCanvasState
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the appropriate state has unsaved changes based on which UI is active
|
||||
pub fn get_has_unsaved_changes_for_state(
|
||||
app_state: &AppState,
|
||||
login_state: &LoginState,
|
||||
register_state: &RegisterState,
|
||||
form_state: &FormState,
|
||||
) -> bool {
|
||||
if app_state.ui.show_login {
|
||||
login_state.has_unsaved_changes() // Uses LegacyCanvasState
|
||||
} else if app_state.ui.show_register {
|
||||
register_state.has_unsaved_changes() // Uses LegacyCanvasState
|
||||
} else {
|
||||
form_state.has_unsaved_changes() // Uses LibraryCanvasState
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current input from the appropriate state based on which UI is active
|
||||
pub fn get_current_input_for_state<'a>(
|
||||
app_state: &AppState,
|
||||
login_state: &'a LoginState,
|
||||
register_state: &'a RegisterState,
|
||||
form_state: &'a FormState,
|
||||
) -> &'a str {
|
||||
if app_state.ui.show_login {
|
||||
login_state.get_current_input() // Uses LegacyCanvasState
|
||||
} else if app_state.ui.show_register {
|
||||
register_state.get_current_input() // Uses LegacyCanvasState
|
||||
} else {
|
||||
form_state.get_current_input() // Uses LibraryCanvasState
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the cursor position for the appropriate state based on which UI is active
|
||||
pub fn set_current_cursor_pos_for_state(
|
||||
app_state: &AppState,
|
||||
login_state: &mut LoginState,
|
||||
register_state: &mut RegisterState,
|
||||
form_state: &mut FormState,
|
||||
pos: usize,
|
||||
) {
|
||||
if app_state.ui.show_login {
|
||||
login_state.set_current_cursor_pos(pos); // Uses LegacyCanvasState
|
||||
} else if app_state.ui.show_register {
|
||||
register_state.set_current_cursor_pos(pos); // Uses LegacyCanvasState
|
||||
} else {
|
||||
form_state.set_current_cursor_pos(pos); // Uses LibraryCanvasState
|
||||
}
|
||||
}
|
||||
|
||||
/// Get cursor position for mixed login/register vs form logic
|
||||
pub fn get_cursor_pos_for_mixed_state(
|
||||
app_state: &AppState,
|
||||
login_state: &LoginState,
|
||||
form_state: &FormState,
|
||||
) -> usize {
|
||||
if app_state.ui.show_login || app_state.ui.show_register {
|
||||
login_state.current_cursor_pos() // Uses LegacyCanvasState
|
||||
} else {
|
||||
form_state.current_cursor_pos() // Uses LibraryCanvasState
|
||||
}
|
||||
}
|
||||
@@ -6,4 +6,3 @@ pub mod admin;
|
||||
pub mod intro;
|
||||
pub mod add_table;
|
||||
pub mod add_logic;
|
||||
pub mod canvas_state;
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
// src/state/pages/canvas_state.rs
|
||||
|
||||
use common::proto::komp_ac::search::search_response::Hit;
|
||||
|
||||
pub trait CanvasState {
|
||||
// --- Existing methods (unchanged) ---
|
||||
fn current_field(&self) -> usize;
|
||||
fn current_cursor_pos(&self) -> usize;
|
||||
fn has_unsaved_changes(&self) -> bool;
|
||||
fn inputs(&self) -> Vec<&String>;
|
||||
fn get_current_input(&self) -> &str;
|
||||
fn get_current_input_mut(&mut self) -> &mut String;
|
||||
fn fields(&self) -> Vec<&str>;
|
||||
fn set_current_field(&mut self, index: usize);
|
||||
fn set_current_cursor_pos(&mut self, pos: usize);
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool);
|
||||
fn get_suggestions(&self) -> Option<&[String]>;
|
||||
fn get_selected_suggestion_index(&self) -> Option<usize>;
|
||||
fn get_rich_suggestions(&self) -> Option<&[Hit]> {
|
||||
None
|
||||
}
|
||||
|
||||
fn get_display_value_for_field(&self, index: usize) -> &str {
|
||||
self.inputs()
|
||||
.get(index)
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("")
|
||||
}
|
||||
fn has_display_override(&self, _index: usize) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -16,11 +16,10 @@ use crate::components::{
|
||||
};
|
||||
use crate::config::colors::themes::Theme;
|
||||
use crate::modes::general::command_navigation::NavigationState;
|
||||
use crate::state::pages::canvas_state::CanvasState as LocalCanvasState; // Keep local one with alias
|
||||
use canvas::canvas::CanvasState; // Import external library's CanvasState trait
|
||||
use canvas::canvas::CanvasState;
|
||||
use crate::state::app::buffer::BufferState;
|
||||
use crate::state::app::highlight::HighlightState as LocalHighlightState; // CHANGED: Alias local version
|
||||
use canvas::canvas::HighlightState as CanvasHighlightState; // CHANGED: Import canvas version with alias
|
||||
use crate::state::app::highlight::HighlightState as LocalHighlightState;
|
||||
use canvas::canvas::HighlightState as CanvasHighlightState;
|
||||
use crate::state::app::state::AppState;
|
||||
use crate::state::pages::admin::AdminState;
|
||||
use crate::state::pages::auth::AuthState;
|
||||
|
||||
@@ -8,9 +8,8 @@ use crate::config::storage::storage::load_auth_data;
|
||||
use crate::modes::common::commands::CommandHandler;
|
||||
use crate::modes::handlers::event::{EventHandler, EventOutcome};
|
||||
use crate::modes::handlers::mode_manager::{AppMode, ModeManager};
|
||||
use crate::state::pages::canvas_state::CanvasState as LocalCanvasState; // Keep local one with alias
|
||||
use canvas::canvas::CanvasState; // Import external library's CanvasState trait
|
||||
use crate::state::pages::form::{FormState, FieldDefinition}; // Import FieldDefinition
|
||||
use canvas::canvas::CanvasState; // Only external library import
|
||||
use crate::state::pages::form::{FormState, FieldDefinition};
|
||||
use crate::state::pages::auth::AuthState;
|
||||
use crate::state::pages::auth::LoginState;
|
||||
use crate::state::pages::auth::RegisterState;
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
use rstest::{fixture, rstest};
|
||||
use std::collections::HashMap;
|
||||
use client::state::pages::form::{FormState, FieldDefinition};
|
||||
use canvas::state::CanvasState
|
||||
use client::state::pages::canvas_state::CanvasState;
|
||||
use canvas::canvas::CanvasState;
|
||||
|
||||
#[fixture]
|
||||
fn test_form_state() -> FormState {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
pub use rstest::{fixture, rstest};
|
||||
pub use client::services::grpc_client::GrpcClient;
|
||||
pub use client::state::pages::form::FormState;
|
||||
pub use client::state::pages::canvas_state::CanvasState;
|
||||
pub use canvas::canvas::CanvasState;
|
||||
pub use prost_types::Value;
|
||||
pub use prost_types::value::Kind;
|
||||
pub use std::collections::HashMap;
|
||||
|
||||
Reference in New Issue
Block a user