Compare commits

...

12 Commits

Author SHA1 Message Date
filipriec
aea2c39215 reverted core actions in canvas 2025-08-20 16:33:19 +02:00
filipriec
4c2464ab30 compiled 2025-08-20 16:28:31 +02:00
filipriec
26053a5fd8 fixes 9 2025-08-20 11:36:56 +02:00
filipriec
589220a2ba fixing 8 2025-08-20 11:33:34 +02:00
filipriec
2cda54633f not working, needs more fixes 2025-08-19 22:14:43 +02:00
filipriec
3eea6b9e88 fixes 7 2025-08-19 22:02:00 +02:00
filipriec
db9bb7e168 fixes 5 2025-08-19 20:05:18 +02:00
filipriec
3ccd094a22 fixing problems more6 2025-08-19 19:21:02 +02:00
filipriec
032f21edaa more canvas implementation4 2025-08-19 14:43:10 +02:00
Priec
42eb087363 canvas fixes3 2025-08-19 13:25:10 +02:00
Priec
d0ff449e3b going into a new canvas library structure 2025-08-19 13:24:48 +02:00
Priec
858f5137d8 migrating to the new canvas library 2025-08-19 10:56:54 +02:00
26 changed files with 518 additions and 1757 deletions

View File

@@ -57,7 +57,7 @@ use canvas::canvas::CanvasState;
use canvas::canvas::CanvasAction;
use canvas::canvas::ActionContext;
use canvas::canvas::HighlightState;
use canvas::canvas::CanvasTheme;
use canvas::CanvasTheme;
use canvas::dispatcher::ActionDispatcher;
use canvas::canvas::ActionResult;
```
@@ -153,7 +153,7 @@ if editor.is_suggestions_active() {
**New rendering:**
```rust
// Canvas handles everything
use canvas::canvas::render_canvas;
use canvas::render_canvas_default;
let active_field_rect = render_canvas(f, area, form_state, theme, edit_mode, highlight_state);

View File

@@ -8,7 +8,7 @@ license.workspace = true
anyhow = { workspace = true }
async-trait = "0.1.88"
common = { path = "../common" }
canvas = { path = "../canvas", features = ["gui"] }
canvas = { path = "../canvas", features = ["gui", "suggestions"] }
ratatui = { workspace = true }
crossterm = { workspace = true }

View File

@@ -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_logic::{AddLogicFocus, AddLogicState};
use canvas::canvas::{render_canvas, CanvasState, HighlightState as CanvasHighlightState}; // Use canvas library
use canvas::{render_canvas, FormEditor};
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Modifier, Style},
@@ -14,15 +14,6 @@ use ratatui::{
use crate::components::common::{dialog, autocomplete}; // Added autocomplete
use crate::config::binds::config::EditorKeybindingMode;
// Helper function to convert between HighlightState types
fn convert_highlight_state(local: &HighlightState) -> CanvasHighlightState {
match local {
HighlightState::Off => CanvasHighlightState::Off,
HighlightState::Characterwise { anchor } => CanvasHighlightState::Characterwise { anchor: *anchor },
HighlightState::Linewise { anchor_line } => CanvasHighlightState::Linewise { anchor_line: *anchor_line },
}
}
pub fn render_add_logic(
f: &mut Frame,
area: Rect,
@@ -30,7 +21,6 @@ pub fn render_add_logic(
app_state: &AppState,
add_logic_state: &mut AddLogicState,
is_edit_mode: bool,
highlight_state: &HighlightState,
) {
let main_block = Block::default()
.title(" Add New Logic Script ")
@@ -168,19 +158,12 @@ pub fn render_add_logic(
| AddLogicFocus::InputDescription
);
let canvas_highlight_state = convert_highlight_state(highlight_state);
let active_field_rect = render_canvas(
f,
canvas_area,
add_logic_state, // AddLogicState implements CanvasState
theme, // Theme implements CanvasTheme
is_edit_mode && focus_on_canvas_inputs,
&canvas_highlight_state,
);
let editor = FormEditor::new(add_logic_state.clone());
let active_field_rect = render_canvas(f, canvas_area, &editor, theme);
// --- Render Autocomplete for Target Column ---
// `is_edit_mode` here refers to the general edit mode of the EventHandler
if is_edit_mode && add_logic_state.current_field() == 1 { // Target Column field
if is_edit_mode && editor.current_field() == 1 { // Target Column field
if add_logic_state.in_target_column_suggestion_mode && add_logic_state.show_target_column_suggestions {
if !add_logic_state.target_column_suggestions.is_empty() {
if let Some(input_rect) = active_field_rect {

View File

@@ -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::{render_canvas_default, render_canvas, FormEditor};
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Modifier, Style},
@@ -13,15 +13,6 @@ use ratatui::{
};
use crate::components::common::dialog;
// Helper function to convert between HighlightState types
fn convert_highlight_state(local: &HighlightState) -> CanvasHighlightState {
match local {
HighlightState::Off => CanvasHighlightState::Off,
HighlightState::Characterwise { anchor } => CanvasHighlightState::Characterwise { anchor: *anchor },
HighlightState::Linewise { anchor_line } => CanvasHighlightState::Linewise { anchor_line: *anchor_line },
}
}
/// Renders the Add New Table page layout, structuring the display of table information,
/// input fields, and action buttons. Adapts layout based on terminal width.
pub fn render_add_table(
@@ -31,7 +22,6 @@ pub fn render_add_table(
app_state: &AppState,
add_table_state: &mut AddTableState,
is_edit_mode: bool, // Determines if canvas inputs are in edit mode
highlight_state: &HighlightState, // For text highlighting in canvas
) {
// --- Configuration ---
// Threshold width to switch between wide and narrow layouts
@@ -357,15 +347,8 @@ pub fn render_add_table(
);
// --- Canvas Rendering (Column Definition Input) - USING CANVAS LIBRARY ---
let canvas_highlight_state = convert_highlight_state(highlight_state);
let _active_field_rect = render_canvas(
f,
canvas_area,
add_table_state, // AddTableState implements CanvasState
theme, // Theme implements CanvasTheme
is_edit_mode && focus_on_canvas_inputs,
&canvas_highlight_state,
);
let editor = FormEditor::new(add_table_state.clone());
let _active_field_rect = render_canvas(f, canvas_area, &editor, theme);
// --- Button Style Helpers ---
let get_button_style = |button_focus: AddTableFocus, current_focus| {

View File

@@ -13,25 +13,21 @@ use ratatui::{
Frame,
};
use crate::state::app::highlight::HighlightState;
use canvas::canvas::{render_canvas, HighlightState as CanvasHighlightState}; // Use canvas library's render function
// Helper function to convert between HighlightState types
fn convert_highlight_state(local: &HighlightState) -> CanvasHighlightState {
match local {
HighlightState::Off => CanvasHighlightState::Off,
HighlightState::Characterwise { anchor } => CanvasHighlightState::Characterwise { anchor: *anchor },
HighlightState::Linewise { anchor_line } => CanvasHighlightState::Linewise { anchor_line: *anchor_line },
}
}
use canvas::{
FormEditor,
render_canvas,
render_suggestions_dropdown,
DefaultCanvasTheme,
};
pub fn render_login(
f: &mut Frame,
area: Rect,
theme: &Theme,
// FIX: take &LoginState (reference), not owned
login_state: &LoginState,
app_state: &AppState,
is_edit_mode: bool,
highlight_state: &HighlightState,
) {
// Main container
let block = Block::default()
@@ -58,15 +54,15 @@ pub fn render_login(
])
.split(inner_area);
// --- FORM RENDERING (Using canvas library directly) ---
let canvas_highlight_state = convert_highlight_state(highlight_state);
render_canvas(
// Wrap LoginState in FormEditor (no clone needed)
let editor = FormEditor::new(login_state.clone());
// Use DefaultCanvasTheme instead of app Theme
let input_rect = render_canvas(
f,
chunks[0],
login_state, // LoginState implements CanvasState
theme, // Theme implements CanvasTheme
is_edit_mode,
&canvas_highlight_state,
&editor,
&DefaultCanvasTheme,
);
// --- ERROR MESSAGE ---
@@ -139,6 +135,19 @@ pub fn render_login(
button_chunks[1],
);
// --- SUGGESTIONS DROPDOWN (if active) ---
if app_state.current_mode == crate::modes::handlers::mode_manager::AppMode::Edit {
if let Some(input_rect) = input_rect {
render_suggestions_dropdown(
f,
f.area(),
input_rect,
&DefaultCanvasTheme,
&editor, // FIX: pass &editor
);
}
}
// --- DIALOG ---
if app_state.ui.dialog.dialog_show {
dialog::render_dialog(

View File

@@ -14,9 +14,8 @@ use ratatui::{
Frame,
};
use crate::state::app::highlight::HighlightState;
use canvas::canvas::{render_canvas, HighlightState as CanvasHighlightState}; // Use canvas library's render function
use canvas::autocomplete::gui::render_autocomplete_dropdown;
use canvas::autocomplete::AutocompleteCanvasState;
use canvas::{FormEditor, render_canvas_default, render_canvas, render_suggestions_dropdown, DefaultCanvasTheme};
use canvas::canvas::HighlightState as CanvasHighlightState;
// Helper function to convert between HighlightState types
fn convert_highlight_state(local: &HighlightState) -> CanvasHighlightState {
@@ -34,7 +33,6 @@ pub fn render_register(
state: &RegisterState,
app_state: &AppState,
is_edit_mode: bool,
highlight_state: &HighlightState,
) {
let block = Block::default()
.borders(Borders::ALL)
@@ -60,15 +58,14 @@ pub fn render_register(
])
.split(inner_area);
// --- FORM RENDERING (Using canvas library directly) ---
let canvas_highlight_state = convert_highlight_state(highlight_state);
// Wrap RegisterState in FormEditor
let editor = FormEditor::new(state.clone());
let input_rect = render_canvas(
f,
chunks[0],
state, // RegisterState implements CanvasState
theme, // Theme implements CanvasTheme
is_edit_mode,
&canvas_highlight_state,
&editor,
theme,
);
// --- HELP TEXT ---
@@ -147,20 +144,18 @@ pub fn render_register(
button_chunks[1],
);
// --- AUTOCOMPLETE DROPDOWN (Using canvas library directly) ---
// --- AUTOCOMPLETE DROPDOWN (Using new canvas suggestions) ---
if app_state.current_mode == AppMode::Edit {
if let Some(autocomplete_state) = state.autocomplete_state() {
if let Some(input_rect) = input_rect {
render_autocomplete_dropdown(
render_suggestions_dropdown(
f,
f.area(), // Frame area
input_rect, // Current input field rect
theme, // Theme implements CanvasTheme
autocomplete_state,
&DefaultCanvasTheme,
&editor,
);
}
}
}
// --- DIALOG ---
if app_state.ui.dialog.dialog_show {

View File

@@ -1,7 +1,5 @@
// src/components/form/form.rs
use crate::components::common::autocomplete;
use crate::config::colors::themes::Theme;
use canvas::canvas::{CanvasState, render_canvas, HighlightState};
use crate::state::pages::form::FormState;
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Margin, Rect},
@@ -9,17 +7,19 @@ use ratatui::{
widgets::{Block, Borders, Paragraph},
Frame,
};
use canvas::canvas::HighlightState;
use canvas::{FormEditor, render_canvas_default, render_canvas, render_suggestions_dropdown, DefaultCanvasTheme};
pub fn render_form(
f: &mut Frame,
area: Rect,
form_state: &FormState,
fields: &[&str],
current_field_idx: &usize,
inputs: &[&String],
fields: &[&str], // no longer needed, FormEditor handles this
current_field_idx: &usize, // no longer needed
inputs: &[&String], // no longer needed
table_name: &str,
theme: &Theme,
is_edit_mode: bool,
is_edit_mode: bool, // FormEditor tracks mode internally
highlight_state: &HighlightState,
total_count: u64,
current_position: u64,
@@ -62,37 +62,24 @@ pub fn render_form(
.alignment(Alignment::Left);
f.render_widget(count_para, main_layout[0]);
// Use the canvas library's render_canvas function
// --- FORM RENDERING (Using new canvas API) ---
let editor = FormEditor::new(form_state.clone());
let active_field_rect = render_canvas(
f,
main_layout[1],
form_state,
&editor,
theme,
is_edit_mode,
highlight_state,
);
// --- RENDER RICH AUTOCOMPLETE ONLY ---
if form_state.autocomplete_active {
// --- SUGGESTIONS DROPDOWN ---
if let Some(active_rect) = active_field_rect {
// Get selected index directly from form_state
let selected_index = form_state.selected_suggestion_index;
// Only render rich suggestions (your Hit objects)
if let Some(rich_suggestions) = form_state.get_rich_suggestions() {
if !rich_suggestions.is_empty() {
autocomplete::render_hit_autocomplete_dropdown(
render_suggestions_dropdown(
f,
main_layout[1],
active_rect,
f.area(),
theme,
rich_suggestions,
selected_index,
form_state,
&DefaultCanvasTheme,
&editor,
);
}
}
// Removed simple suggestions - we only use rich ones now!
}
}
}

View File

@@ -1,6 +1,6 @@
// src/config/colors/themes.rs
use ratatui::style::Color;
use canvas::canvas::CanvasTheme;
use canvas::CanvasTheme;
#[derive(Debug, Clone)]
pub struct Theme {
@@ -108,4 +108,9 @@ impl CanvasTheme for Theme {
fn warning(&self) -> Color {
self.warning
}
fn suggestion_gray(&self) -> Color {
// Neutral gray for suggestions
Color::Rgb(128, 128, 128)
}
}

View File

@@ -1,4 +0,0 @@
// src/client/modes/canvas.rs
pub mod edit;
pub mod common_mode;
pub mod read_only;

View File

@@ -1,500 +0,0 @@
// src/modes/canvas/edit.rs
use crate::config::binds::config::Config;
use crate::modes::handlers::event::EventHandler;
use crate::services::grpc_client::GrpcClient;
use crate::state::app::state::AppState;
use crate::state::pages::admin::AdminState;
use crate::state::pages::{
auth::{LoginState, RegisterState},
form::FormState,
};
use canvas::canvas::CanvasState;
use canvas::{canvas::CanvasAction, dispatcher::ActionDispatcher, canvas::ActionResult};
use anyhow::Result;
use common::proto::komp_ac::search::search_response::Hit;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use tokio::sync::mpsc;
use tracing::info;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EditEventOutcome {
Message(String),
ExitEditMode,
}
/// Helper function to spawn a non-blocking search task for autocomplete.
async fn trigger_form_autocomplete_search(
form_state: &mut FormState,
grpc_client: &mut GrpcClient,
sender: mpsc::UnboundedSender<Vec<Hit>>,
) {
if let Some(field_def) = form_state.fields.get(form_state.current_field) {
if field_def.is_link {
if let Some(target_table) = &field_def.link_target_table {
// 1. Update state for immediate UI feedback
form_state.autocomplete_loading = true;
form_state.autocomplete_active = true;
form_state.autocomplete_suggestions.clear();
form_state.selected_suggestion_index = None;
// 2. Clone everything needed for the background task
let query = form_state.get_current_input().to_string();
let table_to_search = target_table.clone();
let mut grpc_client_clone = grpc_client.clone();
info!(
"[Autocomplete] Spawning search in '{}' for query: '{}'",
table_to_search, query
);
// 3. Spawn the non-blocking task
tokio::spawn(async move {
match grpc_client_clone
.search_table(table_to_search, query)
.await
{
Ok(response) => {
// Send results back through the channel
let _ = sender.send(response.hits);
}
Err(e) => {
tracing::error!(
"[Autocomplete] Search failed: {:?}",
e
);
// Send an empty vec on error so the UI can stop loading
let _ = sender.send(vec![]);
}
}
});
}
}
}
}
pub async fn handle_form_edit_with_canvas(
key_event: KeyEvent,
config: &Config,
form_state: &mut FormState,
ideal_cursor_column: &mut usize,
) -> Result<String> {
// Try canvas action from key first
let canvas_config = canvas::config::CanvasConfig::load();
if let Some(action_name) = canvas_config.get_edit_action(key_event.code, key_event.modifiers) {
let canvas_action = CanvasAction::from_string(action_name);
match ActionDispatcher::dispatch(canvas_action, form_state, ideal_cursor_column).await {
Ok(ActionResult::Success(msg)) => {
return Ok(msg.unwrap_or_default());
}
Ok(ActionResult::HandledByFeature(msg)) => {
return Ok(msg);
}
Ok(ActionResult::Error(msg)) => {
return Ok(format!("Error: {}", msg));
}
Ok(ActionResult::RequiresContext(msg)) => {
return Ok(format!("Context needed: {}", msg));
}
Err(_) => {
// Fall through to try config mapping
}
}
}
// Try config-mapped action
if let Some(action_str) = config.get_edit_action_for_key(key_event.code, key_event.modifiers) {
let canvas_action = CanvasAction::from_string(&action_str);
match ActionDispatcher::dispatch(canvas_action, form_state, ideal_cursor_column).await {
Ok(ActionResult::Success(msg)) => {
return Ok(msg.unwrap_or_default());
}
Ok(ActionResult::HandledByFeature(msg)) => {
return Ok(msg);
}
Ok(ActionResult::Error(msg)) => {
return Ok(format!("Error: {}", msg));
}
Ok(ActionResult::RequiresContext(msg)) => {
return Ok(format!("Context needed: {}", msg));
}
Err(e) => {
return Ok(format!("Action failed: {}", e));
}
}
}
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)),
}
}
/// FIXED: Unified canvas action handler with proper priority order for edit mode
async fn handle_canvas_state_edit<S: CanvasState>(
key: KeyEvent,
config: &Config,
state: &mut S,
ideal_cursor_column: &mut usize,
) -> Result<String> {
// println!("DEBUG: Key pressed: {:?}", key); // DEBUG
// PRIORITY 1: Character insertion in edit mode comes FIRST
if let KeyCode::Char(c) = key.code {
// Only insert if no modifiers or just shift (for uppercase)
if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT {
// println!("DEBUG: Using character insertion priority for: {}", c); // DEBUG
let canvas_action = CanvasAction::InsertChar(c);
match ActionDispatcher::dispatch(canvas_action, state, ideal_cursor_column).await {
Ok(ActionResult::Success(msg)) => {
return Ok(msg.unwrap_or_default());
}
Ok(ActionResult::HandledByFeature(msg)) => {
return Ok(msg);
}
Ok(ActionResult::Error(msg)) => {
return Ok(format!("Error: {}", msg));
}
Ok(ActionResult::RequiresContext(msg)) => {
return Ok(format!("Context needed: {}", msg));
}
Err(e) => {
// println!("DEBUG: Character insertion failed: {:?}, trying config", e);
// Fall through to try config mappings
}
}
}
}
// PRIORITY 2: Check canvas config for special keys/combinations
let canvas_config = canvas::config::CanvasConfig::load();
if let Some(action_name) = canvas_config.get_edit_action(key.code, key.modifiers) {
// println!("DEBUG: Canvas config mapped to: {}", action_name); // DEBUG
let canvas_action = CanvasAction::from_string(action_name);
match ActionDispatcher::dispatch(canvas_action, state, ideal_cursor_column).await {
Ok(ActionResult::Success(msg)) => {
return Ok(msg.unwrap_or_default());
}
Ok(ActionResult::HandledByFeature(msg)) => {
return Ok(msg);
}
Ok(ActionResult::Error(msg)) => {
return Ok(format!("Error: {}", msg));
}
Ok(ActionResult::RequiresContext(msg)) => {
return Ok(format!("Context needed: {}", msg));
}
Err(_) => {
// println!("DEBUG: Canvas action failed, trying client config"); // DEBUG
}
}
} else {
// println!("DEBUG: No canvas config mapping found"); // DEBUG
}
// PRIORITY 3: Check client config ONLY for non-character keys or modified keys
if !matches!(key.code, KeyCode::Char(_)) || !key.modifiers.is_empty() {
if let Some(action_str) = config.get_edit_action_for_key(key.code, key.modifiers) {
// println!("DEBUG: Client config mapped to: {} (for non-char key)", action_str); // DEBUG
let canvas_action = CanvasAction::from_string(&action_str);
match ActionDispatcher::dispatch(canvas_action, state, ideal_cursor_column).await {
Ok(ActionResult::Success(msg)) => {
return Ok(msg.unwrap_or_default());
}
Ok(ActionResult::HandledByFeature(msg)) => {
return Ok(msg);
}
Ok(ActionResult::Error(msg)) => {
return Ok(format!("Error: {}", msg));
}
Ok(ActionResult::RequiresContext(msg)) => {
return Ok(format!("Context needed: {}", msg));
}
Err(e) => {
return Ok(format!("Action failed: {}", e));
}
}
} else {
// println!("DEBUG: No client config mapping found for non-char key"); // DEBUG
}
} else {
// println!("DEBUG: Skipping client config for character key in edit mode"); // DEBUG
}
// println!("DEBUG: No action taken for key: {:?}", key); // DEBUG
Ok(String::new())
}
#[allow(clippy::too_many_arguments)]
pub async fn handle_edit_event(
key: KeyEvent,
config: &Config,
form_state: &mut FormState,
login_state: &mut LoginState,
register_state: &mut RegisterState,
admin_state: &mut AdminState,
current_position: &mut u64,
total_count: u64,
event_handler: &mut EventHandler,
app_state: &AppState,
) -> Result<EditEventOutcome> {
// --- AUTOCOMPLETE-SPECIFIC KEY HANDLING ---
if app_state.ui.show_form && form_state.autocomplete_active {
if let Some(action) =
config.get_edit_action_for_key(key.code, key.modifiers)
{
match action {
"suggestion_down" => {
if !form_state.autocomplete_suggestions.is_empty() {
let current =
form_state.selected_suggestion_index.unwrap_or(0);
let next = (current + 1)
% form_state.autocomplete_suggestions.len();
form_state.selected_suggestion_index = Some(next);
}
return Ok(EditEventOutcome::Message(String::new()));
}
"suggestion_up" => {
if !form_state.autocomplete_suggestions.is_empty() {
let current =
form_state.selected_suggestion_index.unwrap_or(0);
let prev = if current == 0 {
form_state.autocomplete_suggestions.len() - 1
} else {
current - 1
};
form_state.selected_suggestion_index = Some(prev);
}
return Ok(EditEventOutcome::Message(String::new()));
}
"exit" => {
form_state.deactivate_autocomplete();
return Ok(EditEventOutcome::Message(
"Autocomplete cancelled".to_string(),
));
}
"enter_decider" => {
if let Some(selected_idx) =
form_state.selected_suggestion_index
{
if let Some(selection) = form_state
.autocomplete_suggestions
.get(selected_idx)
.cloned()
{
// --- THIS IS THE CORE LOGIC CHANGE ---
// 1. Get the friendly display name for the UI
let display_name =
form_state.get_display_name_for_hit(&selection);
// 2. Store the REAL ID in the form's values
let current_input =
form_state.get_current_input_mut();
*current_input = selection.id.to_string();
// 3. Set the persistent display override in the map
form_state.link_display_map.insert(
form_state.current_field,
display_name,
);
// 4. Finalize state
form_state.deactivate_autocomplete();
form_state.set_has_unsaved_changes(true);
return Ok(EditEventOutcome::Message(
"Selection made".to_string(),
));
}
}
form_state.deactivate_autocomplete();
// Fall through to default 'enter' behavior
}
_ => {} // Let other keys fall through to the live search logic
}
}
}
// --- LIVE AUTOCOMPLETE TRIGGER LOGIC ---
let mut trigger_search = false;
if app_state.ui.show_form {
// Manual trigger
if let Some("trigger_autocomplete") =
config.get_edit_action_for_key(key.code, key.modifiers)
{
if !form_state.autocomplete_active {
trigger_search = true;
}
}
// Live search trigger while typing
else if form_state.autocomplete_active {
if let KeyCode::Char(_) | KeyCode::Backspace = key.code {
let action = if let KeyCode::Backspace = key.code {
"delete_char_backward"
} else {
"insert_char"
};
// FIXED: Use canvas library instead of form_e::execute_edit_action
execute_canvas_action(
action,
key,
form_state,
&mut event_handler.ideal_cursor_column,
)
.await?;
trigger_search = true;
}
}
}
if trigger_search {
trigger_form_autocomplete_search(
form_state,
&mut event_handler.grpc_client,
event_handler.autocomplete_result_sender.clone(),
)
.await;
return Ok(EditEventOutcome::Message("Searching...".to_string()));
}
// --- GENERAL EDIT MODE EVENT HANDLING (IF NOT AUTOCOMPLETE) ---
if let Some(action_str) =
config.get_edit_action_for_key(key.code, key.modifiers)
{
// Handle Enter key (next field)
if action_str == "enter_decider" {
// FIXED: Use canvas library instead of form_e::execute_edit_action
let msg = execute_canvas_action(
"next_field",
key,
form_state,
&mut event_handler.ideal_cursor_column,
)
.await?;
return Ok(EditEventOutcome::Message(msg));
}
// Handle exiting edit mode
if action_str == "exit" {
return Ok(EditEventOutcome::ExitEditMode);
}
// Handle all other edit actions - NOW USING CANVAS LIBRARY
let msg = if app_state.ui.show_login {
// NEW: Use unified canvas handler instead of auth_e::execute_edit_action
handle_canvas_state_edit(
key,
config,
login_state,
&mut event_handler.ideal_cursor_column,
)
.await?
} else if app_state.ui.show_add_table {
// NEW: Use unified canvas handler instead of add_table_e::execute_edit_action
handle_canvas_state_edit(
key,
config,
&mut admin_state.add_table_state,
&mut event_handler.ideal_cursor_column,
)
.await?
} else if app_state.ui.show_add_logic {
// NEW: Use unified canvas handler instead of add_logic_e::execute_edit_action
handle_canvas_state_edit(
key,
config,
&mut admin_state.add_logic_state,
&mut event_handler.ideal_cursor_column,
)
.await?
} else if app_state.ui.show_register {
// NEW: Use unified canvas handler instead of auth_e::execute_edit_action
handle_canvas_state_edit(
key,
config,
register_state,
&mut event_handler.ideal_cursor_column,
)
.await?
} else {
// FIXED: Use canvas library instead of form_e::execute_edit_action
execute_canvas_action(
action_str,
key,
form_state,
&mut event_handler.ideal_cursor_column,
)
.await?
};
return Ok(EditEventOutcome::Message(msg));
}
// --- FALLBACK FOR CHARACTER INSERTION (IF NO OTHER BINDING MATCHED) ---
if let KeyCode::Char(_) = key.code {
let msg = if app_state.ui.show_login {
// NEW: Use unified canvas handler instead of auth_e::execute_edit_action
handle_canvas_state_edit(
key,
config,
login_state,
&mut event_handler.ideal_cursor_column,
)
.await?
} else if app_state.ui.show_add_table {
// NEW: Use unified canvas handler instead of add_table_e::execute_edit_action
handle_canvas_state_edit(
key,
config,
&mut admin_state.add_table_state,
&mut event_handler.ideal_cursor_column,
)
.await?
} else if app_state.ui.show_add_logic {
// NEW: Use unified canvas handler instead of add_logic_e::execute_edit_action
handle_canvas_state_edit(
key,
config,
&mut admin_state.add_logic_state,
&mut event_handler.ideal_cursor_column,
)
.await?
} else if app_state.ui.show_register {
// NEW: Use unified canvas handler instead of auth_e::execute_edit_action
handle_canvas_state_edit(
key,
config,
register_state,
&mut event_handler.ideal_cursor_column,
)
.await?
} else {
// FIXED: Use canvas library instead of form_e::execute_edit_action
execute_canvas_action(
"insert_char",
key,
form_state,
&mut event_handler.ideal_cursor_column,
)
.await?
};
return Ok(EditEventOutcome::Message(msg));
}
Ok(EditEventOutcome::Message(String::new())) // No action taken
}

View File

@@ -1,313 +0,0 @@
// src/modes/canvas/read_only.rs
use crate::config::binds::config::Config;
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::form::FormState;
use crate::state::pages::add_logic::AddLogicState;
use crate::state::pages::add_table::AddTableState;
use crate::state::app::state::AppState;
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,
form_state: &mut FormState,
ideal_cursor_column: &mut usize,
) -> Result<String> {
// Try canvas action from key first
let canvas_config = canvas::config::CanvasConfig::load();
if let Some(action_name) = canvas_config.get_read_only_action(key_event.code, key_event.modifiers) {
let canvas_action = CanvasAction::from_string(action_name);
match ActionDispatcher::dispatch(canvas_action, form_state, ideal_cursor_column).await {
Ok(ActionResult::Success(msg)) => {
return Ok(msg.unwrap_or_default());
}
Ok(ActionResult::HandledByFeature(msg)) => {
return Ok(msg);
}
Ok(ActionResult::Error(msg)) => {
return Ok(format!("Error: {}", msg));
}
Ok(ActionResult::RequiresContext(msg)) => {
return Ok(format!("Context needed: {}", msg));
}
Err(_) => {
// Fall through to try config mapping
}
}
}
// Try config-mapped action
if let Some(action_str) = config.get_read_only_action_for_key(key_event.code, key_event.modifiers) {
let canvas_action = CanvasAction::from_string(&action_str);
match ActionDispatcher::dispatch(canvas_action, form_state, ideal_cursor_column).await {
Ok(ActionResult::Success(msg)) => {
return Ok(msg.unwrap_or_default());
}
Ok(ActionResult::HandledByFeature(msg)) => {
return Ok(msg);
}
Ok(ActionResult::Error(msg)) => {
return Ok(format!("Error: {}", msg));
}
Ok(ActionResult::RequiresContext(msg)) => {
return Ok(format!("Context needed: {}", msg));
}
Err(e) => {
return Ok(format!("Action failed: {}", e));
}
}
}
Ok(String::new())
}
pub async fn handle_read_only_event(
app_state: &mut AppState,
key: KeyEvent,
config: &Config,
form_state: &mut FormState,
login_state: &mut LoginState,
register_state: &mut RegisterState,
add_table_state: &mut AddTableState,
add_logic_state: &mut AddLogicState,
key_sequence_tracker: &mut KeySequenceTracker,
grpc_client: &mut GrpcClient,
command_message: &mut String,
edit_mode_cooldown: &mut bool,
ideal_cursor_column: &mut usize,
) -> Result<(bool, String)> {
if config.is_enter_edit_mode_before(key.code, key.modifiers) {
*edit_mode_cooldown = true;
*command_message = "Entering Edit mode".to_string();
return Ok((false, command_message.clone()));
}
if config.is_enter_edit_mode_after(key.code, key.modifiers) {
// 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();
if !current_input.is_empty() && current_pos < current_input.len() {
login_state.set_current_cursor_pos(current_pos + 1);
*ideal_cursor_column = login_state.current_cursor_pos();
}
} else if app_state.ui.show_add_logic {
let current_input = add_logic_state.get_current_input();
let current_pos = add_logic_state.current_cursor_pos();
if !current_input.is_empty() && current_pos < current_input.len() {
add_logic_state.set_current_cursor_pos(current_pos + 1);
*ideal_cursor_column = add_logic_state.current_cursor_pos();
}
} else if app_state.ui.show_register {
let current_input = register_state.get_current_input();
let current_pos = register_state.current_cursor_pos();
if !current_input.is_empty() && current_pos < current_input.len() {
register_state.set_current_cursor_pos(current_pos + 1);
*ideal_cursor_column = register_state.current_cursor_pos();
}
} else if app_state.ui.show_add_table {
let current_input = add_table_state.get_current_input();
let current_pos = add_table_state.current_cursor_pos();
if !current_input.is_empty() && current_pos < current_input.len() {
add_table_state.set_current_cursor_pos(current_pos + 1);
*ideal_cursor_column = add_table_state.current_cursor_pos();
}
} else {
// 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() {
form_state.set_current_cursor_pos(current_pos + 1);
*ideal_cursor_column = form_state.current_cursor_pos();
}
}
*edit_mode_cooldown = true;
*command_message = "Entering Edit mode (after cursor)".to_string();
return Ok((false, command_message.clone()));
}
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() {
// 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,
login_state,
register_state,
add_table_state,
add_logic_state,
ideal_cursor_column,
).await
};
key_sequence_tracker.reset();
return Ok((false, result));
}
if config.is_key_sequence_prefix(&sequence) {
return Ok((false, command_message.clone()));
}
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() {
// 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,
login_state,
register_state,
add_table_state,
add_logic_state,
ideal_cursor_column,
).await
};
key_sequence_tracker.reset();
return Ok((false, result));
}
}
key_sequence_tracker.reset();
} else {
key_sequence_tracker.reset();
if let Some(action) = config.get_read_only_action_for_key(key.code, key.modifiers).as_deref() {
// 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,
login_state,
register_state,
add_table_state,
add_logic_state,
ideal_cursor_column,
).await
};
return Ok((false, result));
}
}
if !*edit_mode_cooldown {
let default_key = "i".to_string();
let edit_key = config
.keybindings
.read_only
.get("enter_edit_mode_before")
.and_then(|keys| keys.first())
.map(|k| k.to_string())
.unwrap_or(default_key);
*command_message = format!("Read-only mode - press {} to edit", edit_key);
}
*edit_mode_cooldown = false;
Ok((false, command_message.clone()))
}

View File

@@ -2,7 +2,6 @@
use crate::tui::terminal::core::TerminalCore;
use crate::state::app::state::AppState;
use crate::state::pages::{form::FormState, auth::LoginState, auth::RegisterState};
use canvas::canvas::CanvasState;
use anyhow::Result;
pub struct CommandHandler;

View File

@@ -11,7 +11,7 @@ use crate::state::pages::admin::AdminState;
use crate::ui::handlers::context::UiContext;
use crate::modes::handlers::event::EventOutcome;
use crate::modes::general::command_navigation::{handle_command_navigation_event, NavigationState};
use canvas::canvas::CanvasState;
use canvas::DataProvider;
use anyhow::Result;
pub async fn handle_navigation_event(
@@ -90,10 +90,10 @@ pub fn move_up(app_state: &mut AppState, login_state: &mut LoginState, register_
if app_state.focused_button_index == 0 {
app_state.ui.focus_outside_canvas = false;
if app_state.ui.show_login {
let last_field_index = login_state.fields().len().saturating_sub(1);
let last_field_index = login_state.field_count().saturating_sub(1);
login_state.set_current_field(last_field_index);
} else {
let last_field_index = register_state.fields().len().saturating_sub(1);
let last_field_index = register_state.field_count().saturating_sub(1);
register_state.set_current_field(last_field_index);
}
} else {

View File

@@ -10,15 +10,13 @@ use crate::modes::general::command_navigation::{
handle_command_navigation_event, NavigationState,
};
use crate::modes::{
canvas::{common_mode, edit, read_only},
common::{command_mode, commands::CommandHandler},
general::{dialog, navigation},
handlers::mode_manager::{AppMode, ModeManager},
};
use crate::services::auth::AuthClient;
use crate::services::grpc_client::GrpcClient;
use canvas::{canvas::CanvasAction, dispatcher::ActionDispatcher};
use canvas::canvas::CanvasState; // Only need this import now
use canvas::FormEditor;
use crate::state::{
app::{
buffer::{AppView, BufferState},
@@ -756,14 +754,13 @@ impl EventHandler {
if let Some(action) = config.get_common_action(key_code, modifiers) {
match action {
"save" | "force_quit" | "save_and_quit" | "revert" => {
return common_mode::handle_core_action(
return self
.handle_core_action(
action,
form_state,
auth_state,
login_state,
register_state,
&mut self.grpc_client,
&mut self.auth_client,
terminal,
app_state,
)
@@ -775,34 +772,18 @@ impl EventHandler {
// Try canvas action for form first
if app_state.ui.show_form {
let mut editor = FormEditor::new(form_state.clone());
if let Ok(Some(canvas_message)) = self.handle_form_canvas_action(
key_event,
form_state,
&mut editor,
config,
false,
).await {
return Ok(EventOutcome::Ok(canvas_message));
}
}
// Fallback to legacy read-only event handling
let (_should_exit, message) = read_only::handle_read_only_event(
app_state,
key_event,
config,
form_state,
login_state,
register_state,
&mut admin_state.add_table_state,
&mut admin_state.add_logic_state,
&mut self.key_sequence_tracker,
&mut self.grpc_client,
&mut self.command_message,
&mut self.edit_mode_cooldown,
&mut self.ideal_cursor_column,
)
.await?;
return Ok(EventOutcome::Ok(message));
return Ok(EventOutcome::Ok(self.command_message.clone()));
}
AppMode::Highlight => {
@@ -820,24 +801,7 @@ impl EventHandler {
return Ok(EventOutcome::Ok("".to_string()));
}
let (_should_exit, message) =
read_only::handle_read_only_event(
app_state,
key_event,
config,
form_state,
login_state,
register_state,
&mut admin_state.add_table_state,
&mut admin_state.add_logic_state,
&mut self.key_sequence_tracker,
&mut self.grpc_client,
&mut self.command_message,
&mut self.edit_mode_cooldown,
&mut self.ideal_cursor_column,
)
.await?;
return Ok(EventOutcome::Ok(message));
return Ok(EventOutcome::Ok(self.command_message.clone()));
}
AppMode::Edit => {
@@ -845,14 +809,13 @@ impl EventHandler {
if let Some(action) = config.get_common_action(key_code, modifiers) {
match action {
"save" | "force_quit" | "save_and_quit" | "revert" => {
return common_mode::handle_core_action(
return self
.handle_core_action(
action,
form_state,
auth_state,
login_state,
register_state,
&mut self.grpc_client,
&mut self.auth_client,
terminal,
app_state,
)
@@ -864,9 +827,11 @@ impl EventHandler {
// Try canvas action for form first
if app_state.ui.show_form {
let mut editor = FormEditor::new(form_state.clone());
if let Ok(Some(canvas_message)) = self.handle_form_canvas_action(
key_event,
form_state,
&mut editor,
config,
true,
).await {
if !canvas_message.is_empty() {
@@ -876,90 +841,9 @@ impl EventHandler {
}
}
// Handle legacy edit events
let mut current_position = form_state.current_position;
let total_count = form_state.total_count;
let edit_result = edit::handle_edit_event(
key_event,
config,
form_state,
login_state,
register_state,
admin_state,
&mut current_position,
total_count,
self,
app_state,
)
.await;
match edit_result {
Ok(edit::EditEventOutcome::ExitEditMode) => {
self.is_edit_mode = false;
self.edit_mode_cooldown = true;
// Check for unsaved changes across all states
let has_changes = Self::get_has_unsaved_changes_for_state(
app_state,
login_state,
register_state,
form_state
);
// Set appropriate message based on changes
self.command_message = if has_changes {
"Exited edit mode (unsaved changes remain)".to_string()
} else {
"Read-only mode".to_string()
};
terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?;
// Get current input and cursor position
let current_input = Self::get_current_input_for_state(
app_state,
login_state,
register_state,
form_state
);
let current_cursor_pos = Self::get_current_cursor_pos_for_state(
app_state,
login_state,
register_state,
form_state
);
// 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;
Self::set_current_cursor_pos_for_state(
app_state,
login_state,
register_state,
form_state,
new_pos
);
self.ideal_cursor_column = new_pos;
}
return Ok(EventOutcome::Ok(self.command_message.clone()));
}
Ok(edit::EditEventOutcome::Message(msg)) => {
if !msg.is_empty() {
self.command_message = msg;
}
self.key_sequence_tracker.reset();
return Ok(EventOutcome::Ok(self.command_message.clone()));
}
Err(e) => {
return Err(e.into());
}
}
}
AppMode::Command => {
if config.is_exit_command_mode(key_code, modifiers) {
self.command_input.clear();
@@ -1101,67 +985,159 @@ impl EventHandler {
async fn handle_form_canvas_action(
&mut self,
key_event: KeyEvent,
form_state: &mut FormState,
editor: &mut FormEditor<FormState>,
config: &Config,
is_edit_mode: bool,
) -> Result<Option<String>> {
let canvas_config = canvas::config::CanvasConfig::load();
use crossterm::event::KeyCode;
// PRIORITY 1: Handle character insertion in edit mode FIRST
if is_edit_mode {
if let KeyCode::Char(c) = key_event.code {
// Only insert if it's not a special modifier combination
if key_event.modifiers.is_empty() || key_event.modifiers == KeyModifiers::SHIFT {
let canvas_action = CanvasAction::InsertChar(c);
match ActionDispatcher::dispatch(
canvas_action,
form_state,
&mut self.ideal_cursor_column,
).await {
Ok(result) => {
return Ok(Some(result.message().unwrap_or("").to_string()));
}
Err(_) => {
return Ok(Some("Character insertion failed".to_string()));
}
}
}
}
}
// PRIORITY 2: Handle config-mapped actions for non-character keys
let action_str = canvas_config.get_action_for_key(
key_event.code,
key_event.modifiers,
is_edit_mode,
form_state.autocomplete_active,
);
if let Some(action_str) = action_str {
// Skip mode transition actions - let the main event handler deal with them
if Self::is_mode_transition_action(action_str) {
return Ok(None);
}
// Execute the config-mapped action
let canvas_action = CanvasAction::from_string(action_str);
match ActionDispatcher::dispatch(
canvas_action,
form_state,
&mut self.ideal_cursor_column,
).await {
Ok(result) => {
return Ok(Some(result.message().unwrap_or("").to_string()));
}
Err(_) => {
return Ok(Some("Canvas action failed".to_string()));
editor.insert_char(c)?;
return Ok(Some(format!("Inserted '{}'", c)));
}
}
}
// No action found
// Use your config to resolve actions
if let Some(action) = config.get_edit_action_for_key(key_event.code, key_event.modifiers) {
match action {
"delete_char_backward" => { editor.delete_backward()?; return Ok(Some("Deleted backward".to_string())); }
"delete_char_forward" => { editor.delete_forward()?; return Ok(Some("Deleted forward".to_string())); }
"move_left" => { editor.move_left()?; return Ok(Some("Moved left".to_string())); }
"move_right" => { editor.move_right()?; return Ok(Some("Moved right".to_string())); }
"move_up" => { editor.move_up()?; return Ok(Some("Moved up".to_string())); }
"move_down" => { editor.move_down()?; return Ok(Some("Moved down".to_string())); }
"move_line_start" => { editor.move_line_start(); return Ok(Some("Line start".to_string())); }
"move_line_end" => { editor.move_line_end(); return Ok(Some("Line end".to_string())); }
"move_word_next" => { editor.move_word_next(); return Ok(Some("Next word".to_string())); }
"move_word_prev" => { editor.move_word_prev(); return Ok(Some("Prev word".to_string())); }
"move_word_end" => { editor.move_word_end(); return Ok(Some("Word end".to_string())); }
"move_word_end_prev" => { editor.move_word_end_prev(); return Ok(Some("Prev word end".to_string())); }
"next_field" => { editor.next_field()?; return Ok(Some("Next field".to_string())); }
"prev_field" => { editor.prev_field()?; return Ok(Some("Prev field".to_string())); }
"open_suggestions" => {
let field_index = editor.current_field();
editor.open_suggestions(field_index);
return Ok(Some("Opened suggestions".to_string()));
}
"apply_suggestion" | "enter_decider" => {
if let Some(s) = editor.apply_suggestion() {
return Ok(Some(format!("Applied suggestion: {}", s)));
} else {
return Ok(Some("No suggestion applied".to_string()));
}
}
"exit" | "exit_edit_mode" => {
editor.exit_edit_mode()?;
return Ok(Some("Exited edit mode".to_string()));
}
_ => {}
}
}
Ok(None)
}
async fn handle_core_action(
&mut self,
action: &str,
form_state: &mut FormState,
auth_state: &mut AuthState,
login_state: &mut LoginState,
register_state: &mut RegisterState,
terminal: &mut TerminalCore,
app_state: &mut AppState,
) -> Result<EventOutcome> {
match action {
"save" => {
if app_state.ui.show_login {
let message = crate::tui::functions::common::login::save(
auth_state,
login_state,
&mut self.auth_client,
app_state,
)
.await?;
Ok(EventOutcome::Ok(message))
} else {
let save_outcome = crate::tui::functions::common::form::save(
app_state,
form_state,
&mut self.grpc_client,
)
.await?;
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))
}
}
"force_quit" => {
terminal.cleanup()?;
Ok(EventOutcome::Exit(
"Force exiting without saving.".to_string(),
))
}
"save_and_quit" => {
let message = if app_state.ui.show_login {
crate::tui::functions::common::login::save(
auth_state,
login_state,
&mut self.auth_client,
app_state,
)
.await?
} else {
let save_outcome = crate::tui::functions::common::form::save(
app_state,
form_state,
&mut self.grpc_client,
)
.await?;
match save_outcome {
SaveOutcome::NoChange => "No changes to save.".to_string(),
SaveOutcome::UpdatedExisting => "Entry updated.".to_string(),
SaveOutcome::CreatedNew(_) => "New entry created.".to_string(),
}
};
terminal.cleanup()?;
Ok(EventOutcome::Exit(format!(
"{}. Exiting application.",
message
)))
}
"revert" => {
let message = if app_state.ui.show_login {
crate::tui::functions::common::login::revert(login_state, app_state)
.await
} else if app_state.ui.show_register {
crate::tui::functions::common::register::revert(
register_state,
app_state,
)
.await
} else {
crate::tui::functions::common::form::revert(
form_state,
&mut self.grpc_client,
)
.await?
};
Ok(EventOutcome::Ok(message))
}
_ => Ok(EventOutcome::Ok(format!(
"Core action not handled: {}",
action
))),
}
}
fn is_mode_transition_action(action: &str) -> bool {
matches!(action,
"exit" |

View File

@@ -1,2 +0,0 @@
// src/client/modes/highlight.rs
pub mod highlight;

View File

@@ -1,65 +0,0 @@
// src/modes/highlight/highlight.rs
// (This file is intentionally simple for now, reusing ReadOnly logic)
use crate::config::binds::config::Config;
use crate::config::binds::key_sequences::KeySequenceTracker;
use crate::services::grpc_client::GrpcClient;
use crate::state::app::state::AppState;
use crate::state::pages::auth::{LoginState, RegisterState};
use crate::state::pages::admin::AdminState;
use crate::state::pages::form::FormState;
use crate::modes::handlers::event::EventOutcome;
use crate::modes::read_only;
use crossterm::event::KeyEvent;
use anyhow::Result;
/// Handles events when in Highlight mode.
/// Currently, it mostly delegates to the read_only handler for movement.
/// Exiting highlight mode is handled directly in the main event handler.
pub async fn handle_highlight_event(
app_state: &mut AppState,
key: KeyEvent,
config: &Config,
form_state: &mut FormState,
login_state: &mut LoginState,
register_state: &mut RegisterState,
admin_state: &mut AdminState,
key_sequence_tracker: &mut KeySequenceTracker,
current_position: &mut u64,
total_count: u64,
grpc_client: &mut GrpcClient,
command_message: &mut String,
edit_mode_cooldown: &mut bool,
ideal_cursor_column: &mut usize,
) -> Result<EventOutcome> {
// Delegate movement and other actions to the read_only handler
// The rendering logic will use the highlight_anchor to draw the selection
let (should_exit, message) = read_only::handle_read_only_event(
app_state,
key,
config,
form_state,
login_state,
register_state,
&mut admin_state.add_table_state,
&mut admin_state.add_logic_state,
key_sequence_tracker,
grpc_client,
command_message, // Pass the message buffer
edit_mode_cooldown,
ideal_cursor_column,
)
.await?;
// ReadOnly handler doesn't return EventOutcome directly, adapt if needed
// For now, assume Ok outcome unless ReadOnly signals an exit (which we ignore here)
if should_exit {
// This exit is likely for the whole app, let the main loop handle it
// We just return the message from read_only
Ok(EventOutcome::Ok(message))
} else {
Ok(EventOutcome::Ok(message))
}
}

View File

@@ -1,11 +1,10 @@
// src/client/modes/mod.rs
pub mod handlers;
pub mod canvas;
pub mod general;
pub mod common;
pub mod highlight;
pub mod canvas;
pub use handlers::*;
pub use canvas::*;
pub use general::*;
pub use common::*;
pub use canvas::*;

View File

@@ -1,7 +1,7 @@
// src/state/pages/add_logic.rs
use crate::config::binds::config::{EditorConfig, EditorKeybindingMode};
use canvas::canvas::{CanvasState, ActionContext, CanvasAction, AppMode};
use crate::components::common::text_editor::{TextEditor, VimState};
use canvas::{DataProvider, AppMode};
use std::cell::RefCell;
use std::rc::Rc;
use tui_textarea::TextArea;
@@ -277,174 +277,41 @@ impl Default for AddLogicState {
}
}
// Implement external library's CanvasState for AddLogicState
impl CanvasState for AddLogicState {
fn current_field(&self) -> usize {
match self.current_focus {
AddLogicFocus::InputLogicName => 0,
AddLogicFocus::InputTargetColumn => 1,
AddLogicFocus::InputDescription => 2,
// If focus is elsewhere, return the last canvas field used
_ => self.last_canvas_field,
impl DataProvider for AddLogicState {
fn field_count(&self) -> usize {
3 // Logic Name, Target Column, Description
}
fn field_name(&self, index: usize) -> &str {
match index {
0 => "Logic Name",
1 => "Target Column",
2 => "Description",
_ => "",
}
}
fn set_current_field(&mut self, index: usize) {
let new_focus = match index {
0 => AddLogicFocus::InputLogicName,
1 => AddLogicFocus::InputTargetColumn,
2 => AddLogicFocus::InputDescription,
_ => return,
};
if self.current_focus != new_focus {
if self.current_focus == AddLogicFocus::InputTargetColumn {
self.in_target_column_suggestion_mode = false;
self.show_target_column_suggestions = false;
}
self.current_focus = new_focus;
self.last_canvas_field = index;
fn field_value(&self, index: usize) -> &str {
match index {
0 => &self.logic_name_input,
1 => &self.target_column_input,
2 => &self.description_input,
_ => "",
}
}
fn current_cursor_pos(&self) -> usize {
match self.current_focus {
AddLogicFocus::InputLogicName => self.logic_name_cursor_pos,
AddLogicFocus::InputTargetColumn => self.target_column_cursor_pos,
AddLogicFocus::InputDescription => self.description_cursor_pos,
_ => 0,
}
}
fn set_current_cursor_pos(&mut self, pos: usize) {
match self.current_focus {
AddLogicFocus::InputLogicName => {
self.logic_name_cursor_pos = pos.min(self.logic_name_input.len());
}
AddLogicFocus::InputTargetColumn => {
self.target_column_cursor_pos = pos.min(self.target_column_input.len());
}
AddLogicFocus::InputDescription => {
self.description_cursor_pos = pos.min(self.description_input.len());
}
fn set_field_value(&mut self, index: usize, value: String) {
match index {
0 => self.logic_name_input = value,
1 => self.target_column_input = value,
2 => self.description_input = value,
_ => {}
}
}
fn get_current_input(&self) -> &str {
match self.current_focus {
AddLogicFocus::InputLogicName => &self.logic_name_input,
AddLogicFocus::InputTargetColumn => &self.target_column_input,
AddLogicFocus::InputDescription => &self.description_input,
_ => "", // Should not happen if called correctly
}
}
fn get_current_input_mut(&mut self) -> &mut String {
match self.current_focus {
AddLogicFocus::InputLogicName => &mut self.logic_name_input,
AddLogicFocus::InputTargetColumn => &mut self.target_column_input,
AddLogicFocus::InputDescription => &mut self.description_input,
_ => &mut self.logic_name_input, // Fallback
}
}
fn inputs(&self) -> Vec<&String> {
vec![
&self.logic_name_input,
&self.target_column_input,
&self.description_input,
]
}
fn fields(&self) -> Vec<&str> {
vec!["Logic Name", "Target Column", "Description"]
}
fn has_unsaved_changes(&self) -> bool {
self.has_unsaved_changes
}
fn set_has_unsaved_changes(&mut self, changed: bool) {
self.has_unsaved_changes = changed;
}
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
match action {
// Handle saving logic script
CanvasAction::Custom(action_str) if action_str == "save_logic" => {
self.save_logic()
}
// Handle clearing the form
CanvasAction::Custom(action_str) if action_str == "clear_form" => {
self.clear_form()
}
// Handle target column autocomplete activation
CanvasAction::Custom(action_str) if action_str == "activate_autocomplete" => {
if self.current_field() == 1 { // Target Column field
self.in_target_column_suggestion_mode = true;
self.update_target_column_suggestions();
Some("Autocomplete activated".to_string())
} else {
None
}
}
// Handle target column suggestion selection
CanvasAction::Custom(action_str) if action_str == "select_suggestion" => {
if self.current_field() == 1 && self.in_target_column_suggestion_mode {
if let Some(selected_idx) = self.selected_target_column_suggestion_index {
if let Some(suggestion) = self.target_column_suggestions.get(selected_idx) {
self.target_column_input = suggestion.clone();
self.target_column_cursor_pos = suggestion.len();
self.in_target_column_suggestion_mode = false;
self.show_target_column_suggestions = false;
self.has_unsaved_changes = true;
return Some(format!("Selected: {}", suggestion));
}
}
}
None
}
// Custom validation when moving between fields
CanvasAction::NextField => {
match self.current_field() {
0 => { // Logic Name field
if self.logic_name_input.trim().is_empty() {
Some("Logic name cannot be empty".to_string())
} else {
None // Let canvas library handle the normal field movement
}
}
1 => { // Target Column field
// Update suggestions when entering target column field
self.update_target_column_suggestions();
None
}
_ => None,
}
}
// Handle character insertion with validation
CanvasAction::InsertChar(c) => {
if self.current_field() == 1 { // Target Column field
// Update suggestions after character insertion
// Note: Canvas library will handle the actual insertion
// This is just for triggering suggestion updates
None // Let canvas handle insertion, then we'll update suggestions
} else {
None // Let canvas handle normally
}
}
// Let canvas library handle everything else
_ => None,
}
}
fn current_mode(&self) -> AppMode {
self.app_mode
fn supports_suggestions(&self, field_index: usize) -> bool {
// Only Target Column supports suggestions
field_index == 1
}
}

View File

@@ -1,5 +1,6 @@
// src/state/pages/add_table.rs
use canvas::canvas::{CanvasState, ActionContext, CanvasAction, AppMode};
use canvas::{DataProvider, CanvasAction, AppMode};
use ratatui::widgets::TableState;
use serde::{Deserialize, Serialize};
@@ -170,137 +171,40 @@ impl AddTableState {
}
}
// Implement external library's CanvasState for AddTableState
impl CanvasState for AddTableState {
fn current_field(&self) -> usize {
match self.current_focus {
AddTableFocus::InputTableName => 0,
AddTableFocus::InputColumnName => 1,
AddTableFocus::InputColumnType => 2,
// If focus is elsewhere, return the last canvas field used
_ => self.last_canvas_field,
impl DataProvider for AddTableState {
fn field_count(&self) -> usize {
3 // Table name, Column name, Column type
}
fn field_name(&self, index: usize) -> &str {
match index {
0 => "Table name",
1 => "Name",
2 => "Type",
_ => "",
}
}
fn current_cursor_pos(&self) -> usize {
match self.current_focus {
AddTableFocus::InputTableName => self.table_name_cursor_pos,
AddTableFocus::InputColumnName => self.column_name_cursor_pos,
AddTableFocus::InputColumnType => self.column_type_cursor_pos,
_ => 0, // Default if focus is not on an input field
fn field_value(&self, index: usize) -> &str {
match index {
0 => &self.table_name_input,
1 => &self.column_name_input,
2 => &self.column_type_input,
_ => "",
}
}
fn set_current_field(&mut self, index: usize) {
// Update both current focus and last canvas field
self.current_focus = match index {
0 => {
self.last_canvas_field = 0;
AddTableFocus::InputTableName
},
1 => {
self.last_canvas_field = 1;
AddTableFocus::InputColumnName
},
2 => {
self.last_canvas_field = 2;
AddTableFocus::InputColumnType
},
_ => self.current_focus, // Stay on current focus if index is out of bounds
};
fn set_field_value(&mut self, index: usize, value: String) {
match index {
0 => self.table_name_input = value,
1 => self.column_name_input = value,
2 => self.column_type_input = value,
_ => {}
}
self.has_unsaved_changes = true;
}
fn set_current_cursor_pos(&mut self, pos: usize) {
match self.current_focus {
AddTableFocus::InputTableName => self.table_name_cursor_pos = pos,
AddTableFocus::InputColumnName => self.column_name_cursor_pos = pos,
AddTableFocus::InputColumnType => self.column_type_cursor_pos = pos,
_ => {} // Do nothing if focus is not on an input field
}
}
fn get_current_input(&self) -> &str {
match self.current_focus {
AddTableFocus::InputTableName => &self.table_name_input,
AddTableFocus::InputColumnName => &self.column_name_input,
AddTableFocus::InputColumnType => &self.column_type_input,
_ => "", // Should not happen if called correctly
}
}
fn get_current_input_mut(&mut self) -> &mut String {
match self.current_focus {
AddTableFocus::InputTableName => &mut self.table_name_input,
AddTableFocus::InputColumnName => &mut self.column_name_input,
AddTableFocus::InputColumnType => &mut self.column_type_input,
_ => &mut self.table_name_input, // Fallback
}
}
fn inputs(&self) -> Vec<&String> {
vec![&self.table_name_input, &self.column_name_input, &self.column_type_input]
}
fn fields(&self) -> Vec<&str> {
// These must match the order used in render_add_table
vec!["Table name", "Name", "Type"]
}
fn has_unsaved_changes(&self) -> bool {
self.has_unsaved_changes
}
fn set_has_unsaved_changes(&mut self, changed: bool) {
self.has_unsaved_changes = changed;
}
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
match action {
// Handle adding column when user presses Enter on the Add button or uses specific action
CanvasAction::Custom(action_str) if action_str == "add_column" => {
self.add_column_from_inputs()
}
// Handle table saving
CanvasAction::Custom(action_str) if action_str == "save_table" => {
if self.table_name_input.trim().is_empty() {
Some("Table name is required".to_string())
} else if self.columns.is_empty() {
Some("At least one column is required".to_string())
} else {
Some(format!("Saving table: {}", self.table_name_input))
}
}
// Handle deleting selected items
CanvasAction::Custom(action_str) if action_str == "delete_selected" => {
self.delete_selected_items()
}
// Handle canceling (clear form)
CanvasAction::Custom(action_str) if action_str == "cancel" => {
// Reset to defaults but keep profile_name
let profile = self.profile_name.clone();
*self = Self::default();
self.profile_name = profile;
Some("Form cleared".to_string())
}
// Custom validation when moving between fields
CanvasAction::NextField => {
// When leaving table name field, update the table_name for display
if self.current_field() == 0 && !self.table_name_input.trim().is_empty() {
self.table_name = self.table_name_input.trim().to_string();
}
None // Let canvas library handle the normal field movement
}
// Let canvas library handle everything else
_ => None,
}
}
fn current_mode(&self) -> AppMode {
self.app_mode
fn supports_suggestions(&self, _field_index: usize) -> bool {
false // AddTableState doesnt use suggestions
}
}

View File

@@ -1,6 +1,5 @@
// src/state/pages/auth.rs
use canvas::canvas::{CanvasState, ActionContext, CanvasAction, AppMode};
use canvas::autocomplete::{AutocompleteCanvasState, AutocompleteState, SuggestionItem};
use canvas::{DataProvider, AppMode, SuggestionItem};
use lazy_static::lazy_static;
lazy_static! {
@@ -22,6 +21,7 @@ pub struct AuthState {
}
/// Represents the state of the Login form UI
#[derive(Clone)]
pub struct LoginState {
pub username: String,
pub password: String,
@@ -60,8 +60,10 @@ pub struct RegisterState {
pub current_field: usize,
pub current_cursor_pos: usize,
pub has_unsaved_changes: bool,
pub autocomplete: AutocompleteState<String>,
pub app_mode: AppMode,
// Keep role suggestions for later integration
pub role_suggestions: Vec<String>,
pub role_suggestions_active: bool,
}
impl Default for RegisterState {
@@ -76,8 +78,9 @@ impl Default for RegisterState {
current_field: 0,
current_cursor_pos: 0,
has_unsaved_changes: false,
autocomplete: AutocompleteState::new(),
app_mode: AppMode::Edit,
role_suggestions: AVAILABLE_ROLES.clone(),
role_suggestions_active: false,
}
}
}
@@ -95,51 +98,27 @@ impl LoginState {
..Default::default()
}
}
}
impl RegisterState {
pub fn new() -> Self {
let mut state = Self {
autocomplete: AutocompleteState::new(),
app_mode: AppMode::Edit,
..Default::default()
};
// Initialize autocomplete with role suggestions
let suggestions: Vec<SuggestionItem<String>> = AVAILABLE_ROLES
.iter()
.map(|role| SuggestionItem::simple(role.clone(), role.clone()))
.collect();
// Set suggestions but keep inactive initially
state.autocomplete.set_suggestions(suggestions);
state.autocomplete.is_active = false; // Not active by default
state
}
}
// Implement external library's CanvasState for LoginState
impl CanvasState for LoginState {
fn current_field(&self) -> usize {
// Legacy method compatibility
pub fn current_field(&self) -> usize {
self.current_field
}
fn current_cursor_pos(&self) -> usize {
pub fn current_cursor_pos(&self) -> usize {
self.current_cursor_pos
}
fn set_current_field(&mut self, index: usize) {
pub fn set_current_field(&mut self, index: usize) {
if index < 2 {
self.current_field = index;
}
}
fn set_current_cursor_pos(&mut self, pos: usize) {
pub fn set_current_cursor_pos(&mut self, pos: usize) {
self.current_cursor_pos = pos;
}
fn get_current_input(&self) -> &str {
pub fn get_current_input(&self) -> &str {
match self.current_field {
0 => &self.username,
1 => &self.password,
@@ -147,7 +126,7 @@ impl CanvasState for LoginState {
}
}
fn get_current_input_mut(&mut self) -> &mut String {
pub fn get_current_input_mut(&mut self) -> &mut String {
match self.current_field {
0 => &mut self.username,
1 => &mut self.password,
@@ -155,68 +134,57 @@ impl CanvasState for LoginState {
}
}
fn inputs(&self) -> Vec<&String> {
vec![&self.username, &self.password]
pub fn current_mode(&self) -> AppMode {
self.app_mode
}
fn fields(&self) -> Vec<&str> {
vec!["Username/Email", "Password"]
}
fn has_unsaved_changes(&self) -> bool {
// Add missing methods that used to come from CanvasState trait
pub fn has_unsaved_changes(&self) -> bool {
self.has_unsaved_changes
}
fn set_has_unsaved_changes(&mut self, changed: bool) {
pub fn set_has_unsaved_changes(&mut self, changed: bool) {
self.has_unsaved_changes = changed;
}
}
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
match action {
CanvasAction::Custom(action_str) if action_str == "submit" => {
if !self.username.is_empty() && !self.password.is_empty() {
Some(format!("Submitting login for: {}", self.username))
} else {
Some("Please fill in all required fields".to_string())
}
}
_ => None,
impl RegisterState {
pub fn new() -> Self {
Self {
app_mode: AppMode::Edit,
role_suggestions: AVAILABLE_ROLES.clone(),
role_suggestions_active: false,
..Default::default()
}
}
fn current_mode(&self) -> AppMode {
self.app_mode
}
}
// Implement external library's CanvasState for RegisterState
impl CanvasState for RegisterState {
fn current_field(&self) -> usize {
// Legacy method compatibility
pub fn current_field(&self) -> usize {
self.current_field
}
fn current_cursor_pos(&self) -> usize {
pub fn current_cursor_pos(&self) -> usize {
self.current_cursor_pos
}
fn set_current_field(&mut self, index: usize) {
pub fn set_current_field(&mut self, index: usize) {
if index < 5 {
self.current_field = index;
// Auto-activate autocomplete when moving to role field (index 4)
if index == 4 && !self.autocomplete.is_active {
self.activate_autocomplete();
} else if index != 4 && self.autocomplete.is_active {
self.deactivate_autocomplete();
// Auto-activate role suggestions when moving to role field (index 4)
if index == 4 {
self.activate_role_suggestions();
} else {
self.deactivate_role_suggestions();
}
}
}
fn set_current_cursor_pos(&mut self, pos: usize) {
pub fn set_current_cursor_pos(&mut self, pos: usize) {
self.current_cursor_pos = pos;
}
fn get_current_input(&self) -> &str {
pub fn get_current_input(&self) -> &str {
match self.current_field {
0 => &self.username,
1 => &self.email,
@@ -227,7 +195,7 @@ impl CanvasState for RegisterState {
}
}
fn get_current_input_mut(&mut self) -> &mut String {
pub fn get_current_input_mut(&mut self) -> &mut String {
match self.current_field {
0 => &mut self.username,
1 => &mut self.email,
@@ -238,123 +206,121 @@ impl CanvasState for RegisterState {
}
}
fn inputs(&self) -> Vec<&String> {
vec![
&self.username,
&self.email,
&self.password,
&self.password_confirmation,
&self.role,
]
pub fn current_mode(&self) -> AppMode {
self.app_mode
}
fn fields(&self) -> Vec<&str> {
vec![
"Username",
"Email (Optional)",
"Password (Optional)",
"Confirm Password",
"Role (Optional)"
]
// Role suggestions management
pub fn activate_role_suggestions(&mut self) {
self.role_suggestions_active = true;
// Filter suggestions based on current input
let current_input = self.role.to_lowercase();
self.role_suggestions = AVAILABLE_ROLES
.iter()
.filter(|role| role.to_lowercase().contains(&current_input))
.cloned()
.collect();
}
fn has_unsaved_changes(&self) -> bool {
pub fn deactivate_role_suggestions(&mut self) {
self.role_suggestions_active = false;
}
pub fn is_role_suggestions_active(&self) -> bool {
self.role_suggestions_active
}
pub fn get_role_suggestions(&self) -> &[String] {
&self.role_suggestions
}
// Add missing methods that used to come from CanvasState trait
pub fn has_unsaved_changes(&self) -> bool {
self.has_unsaved_changes
}
fn set_has_unsaved_changes(&mut self, changed: bool) {
pub fn set_has_unsaved_changes(&mut self, changed: bool) {
self.has_unsaved_changes = changed;
}
}
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
match action {
CanvasAction::Custom(action_str) if action_str == "submit" => {
if !self.username.is_empty() {
Some(format!("Submitting registration for: {}", self.username))
} else {
Some("Username is required".to_string())
// Step 2: Implement DataProvider for LoginState
impl DataProvider for LoginState {
fn field_count(&self) -> usize {
2
}
}
_ => None,
fn field_name(&self, index: usize) -> &str {
match index {
0 => "Username/Email",
1 => "Password",
_ => "",
}
}
fn current_mode(&self) -> AppMode {
self.app_mode
fn field_value(&self, index: usize) -> &str {
match index {
0 => &self.username,
1 => &self.password,
_ => "",
}
}
// Add autocomplete support for RegisterState
impl AutocompleteCanvasState for RegisterState {
type SuggestionData = String;
fn supports_autocomplete(&self, field_index: usize) -> bool {
field_index == 4 // Only role field supports autocomplete
fn set_field_value(&mut self, index: usize, value: String) {
match index {
0 => self.username = value,
1 => self.password = value,
_ => {}
}
self.has_unsaved_changes = true;
}
fn autocomplete_state(&self) -> Option<&AutocompleteState<Self::SuggestionData>> {
Some(&self.autocomplete)
}
fn autocomplete_state_mut(&mut self) -> Option<&mut AutocompleteState<Self::SuggestionData>> {
Some(&mut self.autocomplete)
}
fn activate_autocomplete(&mut self) {
let current_field = self.current_field();
if self.supports_autocomplete(current_field) {
self.autocomplete.activate(current_field);
// Re-filter suggestions based on current input
let current_input = self.role.to_lowercase();
let filtered_suggestions: Vec<SuggestionItem<String>> = AVAILABLE_ROLES
.iter()
.filter(|role| role.to_lowercase().contains(&current_input))
.map(|role| SuggestionItem::simple(role.clone(), role.clone()))
.collect();
self.autocomplete.set_suggestions(filtered_suggestions);
fn supports_suggestions(&self, _field_index: usize) -> bool {
false // Login form doesn't support suggestions
}
}
fn deactivate_autocomplete(&mut self) {
self.autocomplete.deactivate();
// Step 3: Implement DataProvider for RegisterState
impl DataProvider for RegisterState {
fn field_count(&self) -> usize {
5
}
fn is_autocomplete_active(&self) -> bool {
self.autocomplete.is_active
}
fn is_autocomplete_ready(&self) -> bool {
self.autocomplete.is_ready()
}
fn apply_autocomplete_selection(&mut self) -> Option<String> {
// First, get the data we need and clone it to avoid borrowing conflicts
let selection_info = self.autocomplete.get_selected().map(|selected| {
(selected.value_to_store.clone(), selected.display_text.clone())
});
// Now do the mutable operations
if let Some((value, display_text)) = selection_info {
self.role = value;
self.set_has_unsaved_changes(true);
self.deactivate_autocomplete();
Some(format!("Selected role: {}", display_text))
} else {
None
fn field_name(&self, index: usize) -> &str {
match index {
0 => "Username",
1 => "Email (Optional)",
2 => "Password (Optional)",
3 => "Confirm Password",
4 => "Role (Optional)",
_ => "",
}
}
fn set_autocomplete_suggestions(&mut self, suggestions: Vec<SuggestionItem<Self::SuggestionData>>) {
if let Some(state) = self.autocomplete_state_mut() {
state.set_suggestions(suggestions);
fn field_value(&self, index: usize) -> &str {
match index {
0 => &self.username,
1 => &self.email,
2 => &self.password,
3 => &self.password_confirmation,
4 => &self.role,
_ => "",
}
}
fn set_autocomplete_loading(&mut self, loading: bool) {
if let Some(state) = self.autocomplete_state_mut() {
state.is_loading = loading;
}
fn set_field_value(&mut self, index: usize, value: String) {
match index {
0 => self.username = value,
1 => self.email = value,
2 => self.password = value,
3 => self.password_confirmation = value,
4 => self.role = value,
_ => {}
}
self.has_unsaved_changes = true;
}
fn supports_suggestions(&self, field_index: usize) -> bool {
field_index == 4 // only Role field supports suggestions
}
}

View File

@@ -1,7 +1,8 @@
// src/state/pages/form.rs
use crate::config::colors::themes::Theme;
use canvas::canvas::{CanvasState, CanvasAction, ActionContext, HighlightState, AppMode};
use canvas::{DataProvider, AppMode, EditorState, FormEditor};
use canvas::canvas::HighlightState;
use common::proto::komp_ac::search::search_response::Hit;
use ratatui::layout::Rect;
use ratatui::Frame;
@@ -122,26 +123,19 @@ impl FormState {
area: Rect,
theme: &Theme,
is_edit_mode: bool,
highlight_state: &HighlightState, // Now using canvas::HighlightState
highlight_state: &HighlightState,
) {
let fields_str_slice: Vec<&str> =
self.fields().iter().map(|s| *s).collect();
let values_str_slice: Vec<&String> = self.values.iter().collect();
// Wrap in FormEditor for new API
let mut editor = FormEditor::new(self.clone());
crate::components::form::form::render_form(
f,
area,
self,
&fields_str_slice,
&self.current_field,
&values_str_slice,
&self.table_name,
theme,
is_edit_mode,
highlight_state,
self.total_count,
self.current_position,
);
// Use new canvas rendering
canvas::render_canvas_default(f, area, &editor);
// If autocomplete is active, render suggestions
if self.autocomplete_active && !self.autocomplete_suggestions.is_empty() {
// Note: This will need to be updated when suggestions are integrated
// canvas::render_suggestions_dropdown(f, area, input_rect, &canvas::DefaultCanvasTheme, &editor);
}
}
pub fn reset_to_empty(&mut self) {
@@ -242,97 +236,84 @@ impl FormState {
pub fn set_readonly_mode(&mut self) {
self.app_mode = AppMode::ReadOnly;
}
}
impl CanvasState for FormState {
fn current_field(&self) -> usize {
self.current_field
}
fn current_cursor_pos(&self) -> usize {
self.current_cursor_pos
}
fn has_unsaved_changes(&self) -> bool {
self.has_unsaved_changes
}
fn inputs(&self) -> Vec<&String> {
self.values.iter().collect()
}
fn get_current_input(&self) -> &str {
FormState::get_current_input(self)
}
fn get_current_input_mut(&mut self) -> &mut String {
FormState::get_current_input_mut(self)
}
fn fields(&self) -> Vec<&str> {
// Legacy method compatibility
pub fn fields(&self) -> Vec<&str> {
self.fields
.iter()
.map(|f| f.display_name.as_str())
.collect()
}
fn set_current_field(&mut self, index: usize) {
pub fn get_display_value_for_field(&self, index: usize) -> &str {
if let Some(display_text) = self.link_display_map.get(&index) {
return display_text.as_str();
}
self.values
.get(index)
.map(|s| s.as_str())
.unwrap_or("")
}
pub fn has_display_override(&self, index: usize) -> bool {
self.link_display_map.contains_key(&index)
}
pub fn current_mode(&self) -> AppMode {
self.app_mode
}
// Add missing methods that used to come from CanvasState trait
pub fn has_unsaved_changes(&self) -> bool {
self.has_unsaved_changes
}
pub fn set_has_unsaved_changes(&mut self, changed: bool) {
self.has_unsaved_changes = changed;
}
pub fn current_field(&self) -> usize {
self.current_field
}
pub fn set_current_field(&mut self, index: usize) {
if index < self.fields.len() {
self.current_field = index;
}
self.deactivate_autocomplete();
}
fn set_current_cursor_pos(&mut self, pos: usize) {
pub fn current_cursor_pos(&self) -> usize {
self.current_cursor_pos
}
pub fn set_current_cursor_pos(&mut self, pos: usize) {
self.current_cursor_pos = pos;
}
fn set_has_unsaved_changes(&mut self, changed: bool) {
self.has_unsaved_changes = changed;
}
// --- FEATURE-SPECIFIC ACTION HANDLING ---
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
match action {
CanvasAction::SelectSuggestion => {
if let Some(selected_idx) = self.selected_suggestion_index {
if let Some(hit) = self.autocomplete_suggestions.get(selected_idx).cloned() {
// Extract the value from the selected suggestion
if let Ok(content_map) = serde_json::from_str::<HashMap<String, serde_json::Value>>(&hit.content_json) {
let current_field_def = &self.fields[self.current_field];
if let Some(value) = content_map.get(&current_field_def.data_key) {
let new_value = json_value_to_string(value);
let display_name = self.get_display_name_for_hit(&hit);
*self.get_current_input_mut() = new_value.clone();
self.set_current_cursor_pos(new_value.len());
self.set_has_unsaved_changes(true);
self.deactivate_autocomplete();
return Some(format!("Selected: {}", display_name));
// Step 2: Implement DataProvider for FormState
impl DataProvider for FormState {
fn field_count(&self) -> usize {
self.fields.len()
}
fn field_name(&self, index: usize) -> &str {
&self.fields[index].display_name
}
fn field_value(&self, index: usize) -> &str {
&self.values[index]
}
}
None
}
_ => None, // Let canvas handle other actions
fn set_field_value(&mut self, index: usize, value: String) {
if let Some(v) = self.values.get_mut(index) {
*v = value;
self.has_unsaved_changes = true;
}
}
fn get_display_value_for_field(&self, index: usize) -> &str {
if let Some(display_text) = self.link_display_map.get(&index) {
return display_text.as_str();
}
self.inputs()
.get(index)
.map(|s| s.as_str())
.unwrap_or("")
}
fn has_display_override(&self, index: usize) -> bool {
self.link_display_map.contains_key(&index)
}
fn current_mode(&self) -> AppMode {
self.app_mode
fn supports_suggestions(&self, field_index: usize) -> bool {
self.fields.get(field_index).map(|f| f.is_link).unwrap_or(false)
}
}

View File

@@ -8,7 +8,6 @@ use crate::state::app::buffer::{AppView, BufferState};
use crate::config::storage::storage::{StoredAuthData, save_auth_data};
use crate::ui::handlers::context::DialogPurpose;
use common::proto::komp_ac::auth::LoginResponse;
use canvas::canvas::CanvasState;
use anyhow::{Context, Result};
use tokio::spawn;
use tokio::sync::mpsc;

View File

@@ -8,7 +8,6 @@ use crate::state::{
use crate::ui::handlers::context::DialogPurpose;
use crate::state::app::buffer::{AppView, BufferState};
use common::proto::komp_ac::auth::AuthResponse;
use canvas::canvas::CanvasState;
use anyhow::Context;
use tokio::spawn;
use tokio::sync::mpsc;

View File

@@ -1,7 +1,6 @@
// src/tui/functions/form.rs
use crate::state::pages::form::FormState;
use crate::services::grpc_client::GrpcClient;
use canvas::canvas::CanvasState;
use anyhow::{anyhow, Result};
pub async fn handle_action(

View File

@@ -16,7 +16,6 @@ use crate::components::{
};
use crate::config::colors::themes::Theme;
use crate::modes::general::command_navigation::NavigationState;
use canvas::canvas::CanvasState;
use crate::state::app::buffer::BufferState;
use crate::state::app::highlight::HighlightState as LocalHighlightState;
use canvas::canvas::HighlightState as CanvasHighlightState;
@@ -137,7 +136,6 @@ pub fn render_ui(
register_state,
app_state,
register_state.current_field() < 4, // Now using CanvasState trait method
highlight_state, // Uses local version
);
} else if app_state.ui.show_add_table {
render_add_table(
@@ -147,7 +145,6 @@ pub fn render_ui(
app_state,
&mut admin_state.add_table_state,
is_event_handler_edit_mode,
highlight_state, // Uses local version
);
} else if app_state.ui.show_add_logic {
render_add_logic(
@@ -157,7 +154,6 @@ pub fn render_ui(
app_state,
&mut admin_state.add_logic_state,
is_event_handler_edit_mode,
highlight_state, // Uses local version
);
} else if app_state.ui.show_login {
render_login(
@@ -167,7 +163,6 @@ pub fn render_ui(
login_state,
app_state,
login_state.current_field() < 2, // Now using CanvasState trait method
highlight_state, // Uses local version
);
} else if app_state.ui.show_admin {
crate::components::admin::admin_panel::render_admin_panel(

View File

@@ -8,7 +8,6 @@ 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 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;