From 3488ab4f6b106f785de1f5822effd6e26c0946de Mon Sep 17 00:00:00 2001 From: filipriec Date: Mon, 2 Jun 2025 10:32:39 +0200 Subject: [PATCH] hardcoded adresar to general form --- client/src/components/form/form.rs | 19 +- client/src/functions/modes/edit/form_e.rs | 6 - client/src/modes/canvas/common_mode.rs | 8 - client/src/modes/canvas/edit.rs | 2 +- client/src/modes/handlers/event.rs | 18 +- client/src/services/grpc_client.rs | 193 +++++++++++++----- client/src/services/ui_service.rs | 203 ++++++++++++------- client/src/state/app/state.rs | 23 +-- client/src/state/pages/form.rs | 125 ++++++++---- client/src/tui/functions/common/form.rs | 184 ++++++++++-------- client/src/ui/handlers/render.rs | 3 - client/src/ui/handlers/ui.rs | 227 ++++++++++++++-------- 12 files changed, 636 insertions(+), 375 deletions(-) diff --git a/client/src/components/form/form.rs b/client/src/components/form/form.rs index 2210945..bfe630e 100644 --- a/client/src/components/form/form.rs +++ b/client/src/components/form/form.rs @@ -13,9 +13,9 @@ use crate::components::handlers::canvas::render_canvas; pub fn render_form( f: &mut Frame, area: Rect, - form_state: &impl CanvasState, + form_state_param: &impl CanvasState, fields: &[&str], - current_field: &usize, + current_field_idx: &usize, inputs: &[&String], theme: &Theme, is_edit_mode: bool, @@ -48,7 +48,16 @@ pub fn render_form( .split(inner_area); // Render count/position - let count_position_text = format!("Total: {} | Position: {}", total_count, current_position); + let count_position_text = if total_count == 0 && current_position == 1 { + "Total: 0 | New Entry".to_string() + } else if current_position > total_count && total_count > 0 { + format!("Total: {} | New Entry ({})", total_count, current_position) + } else if total_count == 0 && current_position > 1 { // Should not happen if logic is correct + format!("Total: 0 | New Entry ({})", current_position) + } + else { + format!("Total: {} | Position: {}/{}", total_count, current_position, total_count) + }; let count_para = Paragraph::new(count_position_text) .style(Style::default().fg(theme.fg)) .alignment(Alignment::Left); @@ -58,9 +67,9 @@ pub fn render_form( render_canvas( f, main_layout[1], - form_state, + form_state_param, fields, - current_field, + current_field_idx, inputs, theme, is_edit_mode, diff --git a/client/src/functions/modes/edit/form_e.rs b/client/src/functions/modes/edit/form_e.rs index 6c634c5..ab95b5d 100644 --- a/client/src/functions/modes/edit/form_e.rs +++ b/client/src/functions/modes/edit/form_e.rs @@ -14,8 +14,6 @@ pub async fn execute_common_action( action: &str, state: &mut S, grpc_client: &mut GrpcClient, - current_position: &mut u64, - total_count: u64, ) -> Result { match action { "save" | "revert" => { @@ -30,8 +28,6 @@ pub async fn execute_common_action( let save_result = save( form_state, grpc_client, - current_position, - total_count, ).await; match save_result { @@ -50,8 +46,6 @@ pub async fn execute_common_action( let revert_result = revert( form_state, grpc_client, - current_position, - total_count, ).await; match revert_result { diff --git a/client/src/modes/canvas/common_mode.rs b/client/src/modes/canvas/common_mode.rs index 6723a4f..2774a26 100644 --- a/client/src/modes/canvas/common_mode.rs +++ b/client/src/modes/canvas/common_mode.rs @@ -24,8 +24,6 @@ pub async fn handle_core_action( auth_client: &mut AuthClient, terminal: &mut TerminalCore, app_state: &mut AppState, - current_position: &mut u64, - total_count: u64, ) -> Result { match action { "save" => { @@ -36,8 +34,6 @@ pub async fn handle_core_action( let save_outcome = form_save( form_state, grpc_client, - current_position, - total_count, ).await.context("Register save action failed")?; let message = match save_outcome { SaveOutcome::NoChange => "No changes to save.".to_string(), @@ -58,8 +54,6 @@ pub async fn handle_core_action( let save_outcome = form_save( form_state, grpc_client, - current_position, - total_count, ).await?; match save_outcome { SaveOutcome::NoChange => "No changes to save.".to_string(), @@ -81,8 +75,6 @@ pub async fn handle_core_action( let message = form_revert( form_state, grpc_client, - current_position, - total_count, ).await.context("Form revert x action failed")?; Ok(EventOutcome::Ok(message)) } diff --git a/client/src/modes/canvas/edit.rs b/client/src/modes/canvas/edit.rs index 2774d4b..3aa8978 100644 --- a/client/src/modes/canvas/edit.rs +++ b/client/src/modes/canvas/edit.rs @@ -66,7 +66,7 @@ pub async fn handle_edit_event( // TODO: Implement common actions for AddLogic if needed format!("Action '{}' not implemented for Add Logic in edit mode.", action) } else { // Assuming Form view - let outcome = form_e::execute_common_action(action, form_state, grpc_client, current_position, total_count).await?; + let outcome = form_e::execute_common_action(action, form_state, grpc_client).await?; match outcome { EventOutcome::Ok(msg) | EventOutcome::DataSaved(_, msg) => msg, _ => format!("Unexpected outcome from common action: {:?}", outcome), diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index 53e0771..fcd2878 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -129,8 +129,6 @@ impl EventHandler { admin_state: &mut AdminState, buffer_state: &mut BufferState, app_state: &mut AppState, - total_count: u64, - current_position: &mut u64, ) -> Result { let mut current_mode = ModeManager::derive_mode(app_state, self, admin_state); @@ -484,8 +482,6 @@ impl EventHandler { &mut self.auth_client, terminal, app_state, - current_position, - total_count, ) .await; } @@ -503,8 +499,8 @@ impl EventHandler { &mut admin_state.add_table_state, &mut admin_state.add_logic_state, &mut self.key_sequence_tracker, - current_position, - total_count, + form_state.current_position, + form_state.total_count, grpc_client, &mut self.command_message, &mut self.edit_mode_cooldown, @@ -545,8 +541,8 @@ impl EventHandler { &mut admin_state.add_table_state, &mut admin_state.add_logic_state, &mut self.key_sequence_tracker, - current_position, - total_count, + form_state.current_position, + form_state.total_count, grpc_client, &mut self.command_message, &mut self.edit_mode_cooldown, @@ -570,8 +566,6 @@ impl EventHandler { &mut self.auth_client, terminal, app_state, - current_position, - total_count, ) .await; } @@ -587,8 +581,6 @@ impl EventHandler { register_state, admin_state, &mut self.ideal_cursor_column, - current_position, - total_count, grpc_client, app_state, ) @@ -677,8 +669,6 @@ impl EventHandler { grpc_client, command_handler, terminal, - current_position, - total_count, ) .await?; self.command_mode = false; diff --git a/client/src/services/grpc_client.rs b/client/src/services/grpc_client.rs index 6f6be3b..84c8f83 100644 --- a/client/src/services/grpc_client.rs +++ b/client/src/services/grpc_client.rs @@ -1,101 +1,200 @@ // src/services/grpc_client.rs use tonic::transport::Channel; -use common::proto::multieko2::adresar::adresar_client::AdresarClient; -use common::proto::multieko2::adresar::{AdresarResponse, PostAdresarRequest, PutAdresarRequest}; -use common::proto::multieko2::common::{CountResponse, PositionRequest, Empty}; +use common::proto::multieko2::common::{CountResponse, Empty}; use common::proto::multieko2::table_structure::table_structure_service_client::TableStructureServiceClient; -// Import the new request type for table structure -use common::proto::multieko2::table_structure::{TableStructureResponse, GetTableStructureRequest}; +use common::proto::multieko2::table_structure::{GetTableStructureRequest, TableStructureResponse}; use common::proto::multieko2::table_definition::{ table_definition_client::TableDefinitionClient, - ProfileTreeResponse, PostTableDefinitionRequest, TableDefinitionResponse, + PostTableDefinitionRequest, ProfileTreeResponse, TableDefinitionResponse, }; use common::proto::multieko2::table_script::{ table_script_client::TableScriptClient, PostTableScriptRequest, TableScriptResponse, }; -use anyhow::Result; +use common::proto::multieko2::tables_data::{ + tables_data_client::TablesDataClient, + GetTableDataByPositionRequest, + GetTableDataResponse, + GetTableDataCountRequest, + PostTableDataRequest, PostTableDataResponse, PutTableDataRequest, + PutTableDataResponse, +}; +use anyhow::{Context, Result}; // Added Context +use std::collections::HashMap; // NEW #[derive(Clone)] pub struct GrpcClient { - adresar_client: AdresarClient, table_structure_client: TableStructureServiceClient, table_definition_client: TableDefinitionClient, table_script_client: TableScriptClient, + tables_data_client: TablesDataClient, // NEW } impl GrpcClient { pub async fn new() -> Result { - let adresar_client = AdresarClient::connect("http://[::1]:50051").await?; - let table_structure_client = TableStructureServiceClient::connect("http://[::1]:50051").await?; - let table_definition_client = TableDefinitionClient::connect("http://[::1]:50051").await?; - let table_script_client = TableScriptClient::connect("http://[::1]:50051").await?; + let table_structure_client = TableStructureServiceClient::connect( + "http://[::1]:50051", + ) + .await + .context("Failed to connect to TableStructureService")?; + let table_definition_client = TableDefinitionClient::connect( + "http://[::1]:50051", + ) + .await + .context("Failed to connect to TableDefinitionService")?; + let table_script_client = + TableScriptClient::connect("http://[::1]:50051") + .await + .context("Failed to connect to TableScriptService")?; + let tables_data_client = + TablesDataClient::connect("http://[::1]:50051") + .await + .context("Failed to connect to TablesDataService")?; // NEW Ok(Self { - adresar_client, + // adresar_client, // REMOVE table_structure_client, table_definition_client, table_script_client, + tables_data_client, // NEW }) } - pub async fn get_adresar_count(&mut self) -> Result { - let request = tonic::Request::new(Empty::default()); - let response: CountResponse = self.adresar_client.get_adresar_count(request).await?.into_inner(); - Ok(response.count as u64) - } - - pub async fn get_adresar_by_position(&mut self, position: u64) -> Result { - let request = tonic::Request::new(PositionRequest { position: position as i64 }); - let response: AdresarResponse = self.adresar_client.get_adresar_by_position(request).await?.into_inner(); - Ok(response) - } - - pub async fn post_adresar(&mut self, request: PostAdresarRequest) -> Result> { - let request = tonic::Request::new(request); - let response = self.adresar_client.post_adresar(request).await?; - Ok(response) - } - - pub async fn put_adresar(&mut self, request: PutAdresarRequest) -> Result> { - let request = tonic::Request::new(request); - let response = self.adresar_client.put_adresar(request).await?; - Ok(response) - } - - // Updated get_table_structure method pub async fn get_table_structure( &mut self, profile_name: String, table_name: String, ) -> Result { - // Create the new request type let grpc_request = GetTableStructureRequest { profile_name, table_name, }; let request = tonic::Request::new(grpc_request); - // Call the new gRPC method - let response = self.table_structure_client.get_table_structure(request).await?; + let response = self + .table_structure_client + .get_table_structure(request) + .await + .context("gRPC GetTableStructure call failed")?; Ok(response.into_inner()) } - pub async fn get_profile_tree(&mut self) -> Result { + pub async fn get_profile_tree( + &mut self, + ) -> Result { let request = tonic::Request::new(Empty::default()); - let response = self.table_definition_client.get_profile_tree(request).await?; + let response = self + .table_definition_client + .get_profile_tree(request) + .await + .context("gRPC GetProfileTree call failed")?; Ok(response.into_inner()) } - pub async fn post_table_definition(&mut self, request: PostTableDefinitionRequest) -> Result { + pub async fn post_table_definition( + &mut self, + request: PostTableDefinitionRequest, + ) -> Result { let tonic_request = tonic::Request::new(request); - let response = self.table_definition_client.post_table_definition(tonic_request).await?; + let response = self + .table_definition_client + .post_table_definition(tonic_request) + .await + .context("gRPC PostTableDefinition call failed")?; Ok(response.into_inner()) } - pub async fn post_table_script(&mut self, request: PostTableScriptRequest) -> Result { + pub async fn post_table_script( + &mut self, + request: PostTableScriptRequest, + ) -> Result { let tonic_request = tonic::Request::new(request); - let response = self.table_script_client.post_table_script(tonic_request).await?; + let response = self + .table_script_client + .post_table_script(tonic_request) + .await + .context("gRPC PostTableScript call failed")?; + Ok(response.into_inner()) + } + + // NEW Methods for TablesData service + pub async fn get_table_data_count( + &mut self, + profile_name: String, + table_name: String, + ) -> Result { + let grpc_request = GetTableDataCountRequest { + profile_name, + table_name, + }; + let request = tonic::Request::new(grpc_request); + let response = self + .tables_data_client + .get_table_data_count(request) + .await + .context("gRPC GetTableDataCount call failed")?; + Ok(response.into_inner().count as u64) + } + + pub async fn get_table_data_by_position( + &mut self, + profile_name: String, + table_name: String, + position: i32, + ) -> Result { + let grpc_request = GetTableDataByPositionRequest { + profile_name, + table_name, + position, + }; + let request = tonic::Request::new(grpc_request); + let response = self + .tables_data_client + .get_table_data_by_position(request) + .await + .context("gRPC GetTableDataByPosition call failed")?; + Ok(response.into_inner()) + } + + pub async fn post_table_data( + &mut self, + profile_name: String, + table_name: String, + data: HashMap, + ) -> Result { + let grpc_request = PostTableDataRequest { + profile_name, + table_name, + data, + }; + let request = tonic::Request::new(grpc_request); + let response = self + .tables_data_client + .post_table_data(request) + .await + .context("gRPC PostTableData call failed")?; + Ok(response.into_inner()) + } + + pub async fn put_table_data( + &mut self, + profile_name: String, + table_name: String, + id: i64, + data: HashMap, + ) -> Result { + let grpc_request = PutTableDataRequest { + profile_name, + table_name, + id, + data, + }; + let request = tonic::Request::new(grpc_request); + let response = self + .tables_data_client + .put_table_data(request) + .await + .context("gRPC PutTableData call failed")?; Ok(response.into_inner()) } } diff --git a/client/src/services/ui_service.rs b/client/src/services/ui_service.rs index b0a1986..f5c45c0 100644 --- a/client/src/services/ui_service.rs +++ b/client/src/services/ui_service.rs @@ -90,110 +90,173 @@ impl UiService { } } } - - pub async fn initialize_app_state( + + // MODIFIED: To set initial view table in AppState and return initial column names + pub async fn initialize_app_state_and_form( grpc_client: &mut GrpcClient, app_state: &mut AppState, - ) -> Result> { - // Fetch profile tree - let profile_tree = grpc_client.get_profile_tree().await.context("Failed to get profile tree")?; + // Returns (initial_profile, initial_table, initial_columns) + ) -> Result<(String, String, Vec)> { + let profile_tree = grpc_client + .get_profile_tree() + .await + .context("Failed to get profile tree")?; app_state.profile_tree = profile_tree; - // TODO for general tables and not hardcoded - let default_profile_name = "default".to_string(); - let default_table_name = "2025_test_schema3".to_string(); + // Determine initial table to load (e.g., first table of first profile, or a default) + // For now, let's hardcode a default for simplicity, but this should be more dynamic + let initial_profile_name = app_state + .profile_tree + .profiles + .first() + .map(|p| p.name.clone()) + .unwrap_or_else(|| "default".to_string()); + + let initial_table_name = app_state + .profile_tree + .profiles + .first() + .and_then(|p| p.tables.first().map(|t| t.name.clone())) + .unwrap_or_else(|| "2025_company_data1".to_string()); // Fallback if no tables + + app_state.set_current_view_table( + initial_profile_name.clone(), + initial_table_name.clone(), + ); - // Fetch table structure for the default table let table_structure = grpc_client - .get_table_structure(default_profile_name, default_table_name) + .get_table_structure( + initial_profile_name.clone(), + initial_table_name.clone(), + ) .await - .context("Failed to get initial table structure")?; + .context(format!( + "Failed to get initial table structure for {}.{}", + initial_profile_name, initial_table_name + ))?; - // Extract the column names from the response let column_names: Vec = table_structure .columns .iter() .map(|col| col.name.clone()) .collect(); - Ok(column_names) + Ok((initial_profile_name, initial_table_name, column_names)) } - pub async fn initialize_adresar_count( + // NEW: Fetches and sets count for the current table in FormState + pub async fn fetch_and_set_table_count( grpc_client: &mut GrpcClient, - app_state: &mut AppState, - ) -> Result<()> { - let total_count = grpc_client.get_adresar_count().await.context("Failed to get adresar count")?; - app_state.update_total_count(total_count); - app_state.update_current_position(total_count.saturating_add(1)); // Start in new entry mode - Ok(()) - } - - pub async fn update_adresar_count( - grpc_client: &mut GrpcClient, - app_state: &mut AppState, - ) -> Result<()> { - let total_count = grpc_client.get_adresar_count().await.context("Failed to get adresar by position")?; - app_state.update_total_count(total_count); - Ok(()) - } - - pub async fn load_adresar_by_position( - grpc_client: &mut GrpcClient, - _app_state: &mut AppState, form_state: &mut FormState, - position: u64, + ) -> Result<()> { + let total_count = grpc_client + .get_table_data_count( + form_state.profile_name.clone(), + form_state.table_name.clone(), + ) + .await + .context(format!( + "Failed to get count for table {}.{}", + form_state.profile_name, form_state.table_name + ))?; + form_state.total_count = total_count; + + // Set initial position: if table has items, point to first, else point to new entry + if total_count > 0 { + form_state.current_position = 1; + } else { + form_state.current_position = 1; // For a new entry in an empty table + } + Ok(()) + } + + // MODIFIED: Generic table data loading + pub async fn load_table_data_by_position( + grpc_client: &mut GrpcClient, + form_state: &mut FormState, // Takes &mut FormState to update it + // position is now read from form_state.current_position ) -> Result { - match grpc_client.get_adresar_by_position(position).await { + // Ensure current_position is valid before fetching + if form_state.current_position == 0 || (form_state.total_count > 0 && form_state.current_position > form_state.total_count) { + // This indicates a "new entry" state, no data to load from server. + // The caller should handle this by calling form_state.reset_to_empty() + // or ensuring this function isn't called for a new entry position. + // For now, let's assume reset_to_empty was called if needed. + form_state.reset_to_empty(); // Ensure fields are clear for new entry + return Ok(format!( + "New entry mode for table {}.{}", + form_state.profile_name, form_state.table_name + )); + } + if form_state.total_count == 0 && form_state.current_position == 1 { + // Table is empty, this is the position for a new entry + form_state.reset_to_empty(); + return Ok(format!( + "New entry mode for empty table {}.{}", + form_state.profile_name, form_state.table_name + )); + } + + + match grpc_client + .get_table_data_by_position( + form_state.profile_name.clone(), + form_state.table_name.clone(), + form_state.current_position as i32, + ) + .await + { Ok(response) => { - // Set the ID properly - form_state.id = response.id; - - // Update form values dynamically - form_state.values = vec![ - response.firma, - response.kz, - response.drc, - response.ulica, - response.psc, - response.mesto, - response.stat, - response.banka, - response.ucet, - response.skladm, - response.ico, - response.kontakt, - response.telefon, - response.skladu, - response.fax, - ]; - - form_state.has_unsaved_changes = false; - Ok(format!("Loaded entry {}", position)) + form_state.update_from_response(&response.data); + // ID, values, current_field, current_cursor_pos, has_unsaved_changes are set by update_from_response + Ok(format!( + "Loaded entry {}/{} for table {}.{}", + form_state.current_position, + form_state.total_count, + form_state.profile_name, + form_state.table_name + )) } Err(e) => { - Ok(format!("Error loading entry: {}", e)) + // If loading fails (e.g., record deleted, network error), what should happen? + // Maybe reset to a new entry state or show an error and keep current data. + // For now, log error and return error message. + tracing::error!( + "Error loading entry {} for table {}.{}: {}", + form_state.current_position, + form_state.profile_name, + form_state.table_name, + e + ); + // Potentially clear form or revert to a safe state + // form_state.reset_to_empty(); + Err(anyhow::anyhow!( + "Error loading entry {}: {}", + form_state.current_position, + e + )) } } } - /// Handles the consequences of a save operation, like updating counts. + // MODIFIED: To work with FormState's count and position pub async fn handle_save_outcome( save_outcome: SaveOutcome, - grpc_client: &mut GrpcClient, - app_state: &mut AppState, + _grpc_client: &mut GrpcClient, // May not be needed if count is fetched separately + _app_state: &mut AppState, // May not be needed directly form_state: &mut FormState, ) -> Result<()> { match save_outcome { SaveOutcome::CreatedNew(new_id) => { - // A new record was created, update the count! - UiService::update_adresar_count(grpc_client, app_state).await?; - // Navigate to the new record (now that count is updated) - app_state.update_current_position(app_state.total_count); - form_state.id = new_id; // Ensure ID is set (might be redundant if save already did it) + // form_state.total_count and form_state.current_position should have been updated + // by the `save` function itself. + // Ensure form_state.id is set. + form_state.id = new_id; + // Potentially, re-fetch count to be absolutely sure, but save should be authoritative. + // UiService::fetch_and_set_table_count(grpc_client, form_state).await?; } SaveOutcome::UpdatedExisting | SaveOutcome::NoChange => { - // No count update needed for these outcomes + // No changes to total_count or current_position needed from here. } } Ok(()) diff --git a/client/src/state/app/state.rs b/client/src/state/app/state.rs index bf343b5..936ebaa 100644 --- a/client/src/state/app/state.rs +++ b/client/src/state/app/state.rs @@ -33,11 +33,12 @@ pub struct UiState { pub struct AppState { // Core editor state pub current_dir: String, - pub total_count: u64, - pub current_position: u64, pub profile_tree: ProfileTreeResponse, pub selected_profile: Option, pub current_mode: AppMode, + pub current_view_profile_name: Option, + pub current_view_table_name: Option, + pub focused_button_index: usize, pub pending_table_structure_fetch: Option<(String, String)>, @@ -52,10 +53,10 @@ impl AppState { .to_string(); Ok(AppState { current_dir, - total_count: 0, - current_position: 0, profile_tree: ProfileTreeResponse::default(), selected_profile: None, + current_view_profile_name: None, + current_view_table_name: None, current_mode: AppMode::General, focused_button_index: 0, pending_table_structure_fetch: None, @@ -63,18 +64,14 @@ impl AppState { }) } - // Existing methods remain unchanged - pub fn update_total_count(&mut self, total_count: u64) { - self.total_count = total_count; - } - - pub fn update_current_position(&mut self, current_position: u64) { - self.current_position = current_position; - } - pub fn update_mode(&mut self, mode: AppMode) { self.current_mode = mode; } + + pub fn set_current_view_table(&mut self, profile_name: String, table_name: String) { + self.current_view_profile_name = Some(profile_name); + self.current_view_table_name = Some(table_name); + } // Add dialog helper methods /// Shows a dialog with the given title, message, and buttons. diff --git a/client/src/state/pages/form.rs b/client/src/state/pages/form.rs index 000649e..869c9d9 100644 --- a/client/src/state/pages/form.rs +++ b/client/src/state/pages/form.rs @@ -1,4 +1,6 @@ // src/state/pages/form.rs + +use std::collections::HashMap; // NEW use crate::config::colors::themes::Theme; use ratatui::layout::Rect; use ratatui::Frame; @@ -7,7 +9,13 @@ use crate::state::pages::canvas_state::CanvasState; pub struct FormState { pub id: i64, - pub fields: Vec, + // NEW fields for dynamic table context + pub profile_name: String, + pub table_name: String, + pub total_count: u64, + pub current_position: u64, // 1-based index, 0 or total_count + 1 for new entry + + pub fields: Vec, // Already dynamic, which is good pub values: Vec, pub current_field: usize, pub has_unsaved_changes: bool, @@ -15,11 +23,19 @@ pub struct FormState { } impl FormState { - /// Create a new FormState with dynamic fields. - pub fn new(fields: Vec) -> Self { - let values = vec![String::new(); fields.len()]; // Initialize values for each field + // MODIFIED constructor + pub fn new( + profile_name: String, + table_name: String, + fields: Vec, + ) -> Self { + let values = vec![String::new(); fields.len()]; FormState { - id: 0, + id: 0, // Default to 0, indicating a new or unloaded record + profile_name, + table_name, + total_count: 0, // Will be fetched after initialization + current_position: 0, // Will be set after count is fetched (e.g., 1 or total_count + 1) fields, values, current_field: 0, @@ -35,31 +51,42 @@ impl FormState { theme: &Theme, is_edit_mode: bool, highlight_state: &HighlightState, - total_count: u64, - current_position: u64, + // total_count and current_position are now part of self ) { - let fields: Vec<&str> = self.fields.iter().map(|s| s.as_str()).collect(); - let values: Vec<&String> = self.values.iter().collect(); + let fields_str_slice: Vec<&str> = + self.fields.iter().map(|s| s.as_str()).collect(); + let values_str_slice: Vec<&String> = self.values.iter().collect(); crate::components::form::form::render_form( f, area, - self, - &fields, + self, // Pass self as CanvasState + &fields_str_slice, &self.current_field, - &values, + &values_str_slice, theme, is_edit_mode, highlight_state, - total_count, - current_position, + self.total_count, // MODIFIED: Use self.total_count + self.current_position, // MODIFIED: Use self.current_position ); } + // MODIFIED: Reset now also considers table context for counts pub fn reset_to_empty(&mut self) { - self.id = 0; // Reset ID to 0 for new entries - self.values.iter_mut().for_each(|v| v.clear()); // Clear all values + self.id = 0; + self.values.iter_mut().for_each(|v| v.clear()); + self.current_field = 0; + self.current_cursor_pos = 0; self.has_unsaved_changes = false; + // current_position should be set to total_count + 1 for a new entry + // This might be better handled by the logic that calls reset_to_empty + // For now, let's ensure it's consistent with a "new" state. + if self.total_count > 0 { + self.current_position = self.total_count + 1; + } else { + self.current_position = 1; // If table is empty, new record is at position 1 + } } pub fn get_current_input(&self) -> &str { @@ -75,15 +102,43 @@ impl FormState { .expect("Invalid current_field index") } - pub fn update_from_response(&mut self, response: common::proto::multieko2::adresar::AdresarResponse) { - self.id = response.id; - self.values = vec![ - response.firma, response.kz, response.drc, - response.ulica, response.psc, response.mesto, - response.stat, response.banka, response.ucet, - response.skladm, response.ico, response.kontakt, - response.telefon, response.skladu, response.fax, - ]; + // MODIFIED: Update from a generic HashMap response + pub fn update_from_response( + &mut self, + response_data: &HashMap, + ) { + self.values = self.fields + .iter() + .map(|field_name| { + response_data.get(field_name).cloned().unwrap_or_default() + }) + .collect(); + + if let Some(id_str) = response_data.get("id") { + match id_str.parse::() { + Ok(parsed_id) => self.id = parsed_id, + Err(e) => { + tracing::error!( + "Failed to parse 'id' field '{}' for table {}.{}: {}", + id_str, + self.profile_name, + self.table_name, + e + ); + self.id = 0; // Default to 0 if parsing fails + } + } + } else { + // If no ID is present, it might be a new record structure or an error + // For now, assume it means the record doesn't have an ID from the server yet + self.id = 0; + } + self.has_unsaved_changes = false; + // current_field and current_cursor_pos might need resetting or adjusting + // depending on the desired behavior after loading data. + // For now, let's reset current_field to 0. + self.current_field = 0; + self.current_cursor_pos = 0; } } @@ -105,31 +160,26 @@ impl CanvasState for FormState { } fn get_current_input(&self) -> &str { - self.values - .get(self.current_field) - .map(|s| s.as_str()) - .unwrap_or("") + // Re-use the struct's own method + FormState::get_current_input(self) } fn get_current_input_mut(&mut self) -> &mut String { - self.values - .get_mut(self.current_field) - .expect("Invalid current_field index") + // Re-use the struct's own method + FormState::get_current_input_mut(self) } fn fields(&self) -> Vec<&str> { self.fields.iter().map(|s| s.as_str()).collect() } - // --- Implement the setter methods --- fn set_current_field(&mut self, index: usize) { - if index < self.fields.len() { // Basic bounds check + if index < self.fields.len() { self.current_field = index; } } fn set_current_cursor_pos(&mut self, pos: usize) { - // Optional: Add validation based on current input length if needed self.current_cursor_pos = pos; } @@ -137,12 +187,11 @@ impl CanvasState for FormState { self.has_unsaved_changes = changed; } - // --- Autocomplete Support (Not Used for FormState) --- fn get_suggestions(&self) -> Option<&[String]> { - None // FormState doesn't provide suggestions + None } fn get_selected_suggestion_index(&self) -> Option { - None // FormState doesn't have selected suggestions + None } } diff --git a/client/src/tui/functions/common/form.rs b/client/src/tui/functions/common/form.rs index 2ff2d37..f5aed75 100644 --- a/client/src/tui/functions/common/form.rs +++ b/client/src/tui/functions/common/form.rs @@ -2,114 +2,130 @@ use crate::services::grpc_client::GrpcClient; use crate::state::pages::form::FormState; -use common::proto::multieko2::adresar::{PostAdresarRequest, PutAdresarRequest}; -use anyhow::Result; +use anyhow::{Context, Result}; // Added Context +use std::collections::HashMap; // NEW #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SaveOutcome { - NoChange, // Nothing needed saving - UpdatedExisting, // An existing record was updated - CreatedNew(i64), // A new record was created (include its new ID) + NoChange, + UpdatedExisting, + CreatedNew(i64), // Keep the ID } -/// Shared logic for saving the current form state +// MODIFIED save function pub async fn save( form_state: &mut FormState, grpc_client: &mut GrpcClient, - current_position: &mut u64, - total_count: u64, -) -> Result { // <-- Return SaveOutcome +) -> Result { if !form_state.has_unsaved_changes { - return Ok(SaveOutcome::NoChange); // Early exit if no changes + return Ok(SaveOutcome::NoChange); } - let is_new = *current_position == total_count + 1; - let outcome = if is_new { - let post_request = PostAdresarRequest { - firma: form_state.values[0].clone(), - kz: form_state.values[1].clone(), - drc: form_state.values[2].clone(), - ulica: form_state.values[3].clone(), - psc: form_state.values[4].clone(), - mesto: form_state.values[5].clone(), - stat: form_state.values[6].clone(), - banka: form_state.values[7].clone(), - ucet: form_state.values[8].clone(), - skladm: form_state.values[9].clone(), - ico: form_state.values[10].clone(), - kontakt: form_state.values[11].clone(), - telefon: form_state.values[12].clone(), - skladu: form_state.values[13].clone(), - fax: form_state.values[14].clone(), - }; - let response = grpc_client.post_adresar(post_request).await?; - let new_id = response.into_inner().id; - form_state.id = new_id; - SaveOutcome::CreatedNew(new_id) // <-- Return CreatedNew with ID + let data_map: HashMap = form_state + .fields + .iter() + .zip(form_state.values.iter()) + .map(|(field, value)| (field.clone(), value.clone())) + .collect(); + + let outcome: SaveOutcome; + + let is_new_entry = form_state.id == 0 || (form_state.total_count > 0 && form_state.current_position > form_state.total_count) || (form_state.total_count == 0 && form_state.current_position == 1) ; + + + if is_new_entry { + let response = grpc_client + .post_table_data( + form_state.profile_name.clone(), + form_state.table_name.clone(), + data_map, + ) + .await + .context("Failed to post new table data")?; + + if response.success { + form_state.id = response.inserted_id; + // After creating a new entry, total_count increases, and current_position becomes this new total_count + form_state.total_count += 1; + form_state.current_position = form_state.total_count; + outcome = SaveOutcome::CreatedNew(response.inserted_id); + } else { + return Err(anyhow::anyhow!( + "Server failed to insert data: {}", + response.message + )); + } } else { - let put_request = PutAdresarRequest { - id: form_state.id, - firma: form_state.values[0].clone(), - kz: form_state.values[1].clone(), - drc: form_state.values[2].clone(), - ulica: form_state.values[3].clone(), - psc: form_state.values[4].clone(), - mesto: form_state.values[5].clone(), - stat: form_state.values[6].clone(), - banka: form_state.values[7].clone(), - ucet: form_state.values[8].clone(), - skladm: form_state.values[9].clone(), - ico: form_state.values[10].clone(), - kontakt: form_state.values[11].clone(), - telefon: form_state.values[12].clone(), - skladu: form_state.values[13].clone(), - fax: form_state.values[14].clone(), - }; - let _ = grpc_client.put_adresar(put_request).await?; - SaveOutcome::UpdatedExisting - }; + // This assumes form_state.id is valid for an existing record + if form_state.id == 0 { + return Err(anyhow::anyhow!( + "Cannot update record: ID is 0, but not classified as new entry." + )); + } + let response = grpc_client + .put_table_data( + form_state.profile_name.clone(), + form_state.table_name.clone(), + form_state.id, + data_map, + ) + .await + .context("Failed to put (update) table data")?; + + if response.success { + outcome = SaveOutcome::UpdatedExisting; + } else { + return Err(anyhow::anyhow!( + "Server failed to update data: {}", + response.message + )); + } + } form_state.has_unsaved_changes = false; Ok(outcome) } -/// Discard changes since last save pub async fn revert( - form_state: &mut FormState, + form_state: &mut FormState, // Takes &mut FormState to update it grpc_client: &mut GrpcClient, - current_position: &mut u64, - total_count: u64, ) -> Result { - let is_new = *current_position == total_count + 1; - - if is_new { - // Clear all fields for new entries - form_state.values.iter_mut().for_each(|v| *v = String::new()); - form_state.has_unsaved_changes = false; + if form_state.id == 0 || (form_state.total_count > 0 && form_state.current_position > form_state.total_count) || (form_state.total_count == 0 && form_state.current_position == 1) { + let old_total_count = form_state.total_count; // Preserve for correct new position + form_state.reset_to_empty(); // reset_to_empty will clear values and set id=0 + form_state.total_count = old_total_count; // Restore total_count + if form_state.total_count > 0 { // Correctly set current_position for new + form_state.current_position = form_state.total_count + 1; + } else { + form_state.current_position = 1; + } return Ok("New entry cleared".to_string()); } - let data = grpc_client.get_adresar_by_position(*current_position).await?; + if form_state.current_position == 0 || form_state.current_position > form_state.total_count { + if form_state.total_count > 0 { + form_state.current_position = 1; + } else { + // No records to revert to, effectively a new entry state. + form_state.reset_to_empty(); + return Ok("No saved data to revert to; form cleared.".to_string()); + } + } - // Update form fields with saved values - form_state.values = vec![ - data.firma, - data.kz, - data.drc, - data.ulica, - data.psc, - data.mesto, - data.stat, - data.banka, - data.ucet, - data.skladm, - data.ico, - data.kontakt, - data.telefon, - data.skladu, - data.fax, - ]; + let response = grpc_client + .get_table_data_by_position( + form_state.profile_name.clone(), + form_state.table_name.clone(), + form_state.current_position as i32, + ) + .await + .context(format!( + "Failed to get table data by position {} for table {}.{}", + form_state.current_position, + form_state.profile_name, + form_state.table_name + ))?; - form_state.has_unsaved_changes = false; + form_state.update_from_response(&response.data); Ok("Changes discarded, reloaded last saved version".to_string()) } diff --git a/client/src/ui/handlers/render.rs b/client/src/ui/handlers/render.rs index 34d5b1a..3e2f01b 100644 --- a/client/src/ui/handlers/render.rs +++ b/client/src/ui/handlers/render.rs @@ -47,8 +47,6 @@ pub fn render_ui( event_handler_command_mode_active: bool, event_handler_command_message: &str, navigation_state: &NavigationState, - total_count: u64, - current_position: u64, current_dir: &str, current_fps: f64, app_state: &AppState, @@ -163,7 +161,6 @@ pub fn render_ui( render_form( f, form_render_area, form_state, &fields_vec, &form_state.current_field, &values_vec, theme, is_event_handler_edit_mode, highlight_state, - total_count, current_position, ); } diff --git a/client/src/ui/handlers/ui.rs b/client/src/ui/handlers/ui.rs index f0e7298..994b8d3 100644 --- a/client/src/ui/handlers/ui.rs +++ b/client/src/ui/handlers/ui.rs @@ -1,4 +1,4 @@ -// client/src/ui/handlers/ui.rs +// src/ui/handlers/ui.rs use crate::config::binds::config::Config; use crate::config::colors::themes::Theme; @@ -27,36 +27,32 @@ use crate::ui::handlers::context::DialogPurpose; use crate::tui::functions::common::login; use crate::tui::functions::common::register; use std::time::Instant; -use anyhow::{Context, Result}; +use anyhow::{anyhow, Context, Result}; use crossterm::cursor::SetCursorStyle; use crossterm::event as crossterm_event; use tracing::{error, info, warn}; use tokio::sync::mpsc; - pub async fn run_ui() -> Result<()> { let config = Config::load().context("Failed to load configuration")?; let theme = Theme::from_str(&config.colors.theme); let mut terminal = TerminalCore::new().context("Failed to initialize terminal")?; - let mut grpc_client = GrpcClient::new().await?; + let mut grpc_client = GrpcClient::new().await.context("Failed to create GrpcClient")?; let mut command_handler = CommandHandler::new(); - - let (login_result_sender, mut login_result_receiver) = - mpsc::channel::(1); - let (register_result_sender, mut register_result_receiver) = - mpsc::channel::(1); - let (save_table_result_sender, mut save_table_result_receiver) = - mpsc::channel::>(1); - let (save_logic_result_sender, _save_logic_result_receiver) = - mpsc::channel::>(1); + let (login_result_sender, mut login_result_receiver) = mpsc::channel::(1); + let (register_result_sender, mut register_result_receiver) = mpsc::channel::(1); + let (save_table_result_sender, mut save_table_result_receiver) = mpsc::channel::>(1); + let (save_logic_result_sender, _save_logic_result_receiver) = mpsc::channel::>(1); let mut event_handler = EventHandler::new( login_result_sender.clone(), register_result_sender.clone(), save_table_result_sender.clone(), save_logic_result_sender.clone(), - ).await.context("Failed to create event handler")?; + ) + .await + .context("Failed to create event handler")?; let event_reader = EventReader::new(); let mut auth_state = AuthState::default(); @@ -67,7 +63,6 @@ pub async fn run_ui() -> Result<()> { let mut buffer_state = BufferState::default(); let mut app_state = AppState::new().context("Failed to create initial app state")?; - let mut auto_logged_in = false; match load_auth_data() { Ok(Some(stored_data)) => { @@ -86,14 +81,33 @@ pub async fn run_ui() -> Result<()> { } } + // Initialize AppState and FormState with table data + let (initial_profile, initial_table, initial_columns) = + UiService::initialize_app_state_and_form(&mut grpc_client, &mut app_state) + .await + .context("Failed to initialize app state and form")?; - let column_names = - UiService::initialize_app_state(&mut grpc_client, &mut app_state) - .await.context("Failed to initialize app state from UI service")?; - let mut form_state = FormState::new(column_names); + let mut form_state = FormState::new( + initial_profile.clone(), + initial_table.clone(), + initial_columns, + ); - UiService::initialize_adresar_count(&mut grpc_client, &mut app_state).await?; - form_state.reset_to_empty(); + UiService::fetch_and_set_table_count(&mut grpc_client, &mut form_state) + .await + .context(format!( + "Failed to fetch initial count for table {}.{}", + initial_profile, initial_table + ))?; + + // Load initial data for the form + if form_state.total_count > 0 { + if let Err(e) = UiService::load_table_data_by_position(&mut grpc_client, &mut form_state).await { + event_handler.command_message = format!("Error loading initial data: {}", e); + } + } else { + form_state.reset_to_empty(); + } if auto_logged_in { buffer_state.history = vec![AppView::Form]; @@ -104,9 +118,10 @@ pub async fn run_ui() -> Result<()> { let mut last_frame_time = Instant::now(); let mut current_fps = 0.0; let mut needs_redraw = true; + let mut prev_view_profile_name = app_state.current_view_profile_name.clone(); + let mut prev_view_table_name = app_state.current_view_table_name.clone(); loop { - if let Some(active_view) = buffer_state.get_active_view() { app_state.ui.show_intro = false; app_state.ui.show_login = false; @@ -154,14 +169,53 @@ pub async fn run_ui() -> Result<()> { } } + // Handle table change for FormView + if app_state.ui.show_form { + let current_view_profile = app_state.current_view_profile_name.clone(); + let current_view_table = app_state.current_view_table_name.clone(); + if prev_view_profile_name != current_view_profile || prev_view_table_name != current_view_table { + if let (Some(prof_name), Some(tbl_name)) = (current_view_profile.as_ref(), current_view_table.as_ref()) { + app_state.show_loading_dialog("Loading Table", &format!("Fetching data for {}.{}...", prof_name, tbl_name)); + needs_redraw = true; + + match grpc_client.get_table_structure(prof_name.clone(), tbl_name.clone()).await { + Ok(structure_response) => { + let new_columns: Vec = structure_response.columns.iter().map(|c| c.name.clone()).collect(); + form_state = FormState::new(prof_name.clone(), tbl_name.clone(), new_columns); + + if let Err(e) = UiService::fetch_and_set_table_count(&mut grpc_client, &mut form_state).await { + app_state.update_dialog_content(&format!("Error fetching count: {}", e), vec!["OK".to_string()], DialogPurpose::LoginFailed); + } else { + if form_state.total_count > 0 { + if let Err(e) = UiService::load_table_data_by_position(&mut grpc_client, &mut form_state).await { + app_state.update_dialog_content(&format!("Error loading data: {}", e), vec!["OK".to_string()], DialogPurpose::LoginFailed); + } else { + app_state.hide_dialog(); + } + } else { + form_state.reset_to_empty(); + app_state.hide_dialog(); + } + } + } + Err(e) => { + app_state.update_dialog_content(&format!("Error fetching table structure: {}", e), vec!["OK".to_string()], DialogPurpose::LoginFailed); + app_state.current_view_profile_name = prev_view_profile_name.clone(); + app_state.current_view_table_name = prev_view_table_name.clone(); + } + } + } + prev_view_profile_name = current_view_profile; + prev_view_table_name = current_view_table; + needs_redraw = true; + } + } if let Some((profile_name, table_name)) = app_state.pending_table_structure_fetch.take() { if app_state.ui.show_add_logic { - if admin_state.add_logic_state.profile_name == profile_name && admin_state.add_logic_state.selected_table_name.as_deref() == Some(table_name.as_str()) { - info!("Fetching table structure for {}.{}", profile_name, table_name); let fetch_message = UiService::initialize_add_logic_table_data( &mut grpc_client, @@ -194,7 +248,6 @@ pub async fn run_ui() -> Result<()> { } } - if needs_redraw { terminal.draw(|f| { render_ui( @@ -213,9 +266,6 @@ pub async fn run_ui() -> Result<()> { event_handler.command_mode, &event_handler.command_message, &event_handler.navigation_state, - - app_state.total_count, - app_state.current_position, &app_state.current_dir, current_fps, &app_state, @@ -224,11 +274,10 @@ pub async fn run_ui() -> Result<()> { needs_redraw = false; } - if let Some(table_name) = admin_state.add_logic_state.script_editor_awaiting_column_autocomplete.clone() { if app_state.ui.show_add_logic { let profile_name = admin_state.add_logic_state.profile_name.clone(); - + info!("Fetching columns for table selection: {}.{}", profile_name, table_name); match UiService::fetch_columns_for_table(&mut grpc_client, &profile_name, &table_name).await { Ok(columns) => { @@ -247,7 +296,6 @@ pub async fn run_ui() -> Result<()> { } } - let current_mode = ModeManager::derive_mode(&app_state, &event_handler, &admin_state); match current_mode { AppMode::Edit => { terminal.show_cursor()?; } @@ -264,15 +312,12 @@ pub async fn run_ui() -> Result<()> { AppMode::Command => { terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; terminal.show_cursor().context("Failed to show cursor in Command mode")?; } } + let position_before_event = form_state.current_position; - let total_count = app_state.total_count; - let mut current_position = app_state.current_position; - let position_before_event = current_position; if app_state.ui.dialog.is_loading { needs_redraw = true; } - let mut event_outcome_result = Ok(EventOutcome::Ok(String::new())); let mut event_processed = false; if crossterm_event::poll(std::time::Duration::from_millis(1))? { @@ -292,16 +337,12 @@ pub async fn run_ui() -> Result<()> { &mut admin_state, &mut buffer_state, &mut app_state, - total_count, - &mut current_position, ).await; } if event_processed { needs_redraw = true; } - app_state.current_position = current_position; - match login_result_receiver.try_recv() { Ok(result) => { @@ -315,7 +356,6 @@ pub async fn run_ui() -> Result<()> { } } - match register_result_receiver.try_recv() { Ok(result) => { if register::handle_registration_result(result, &mut app_state, &mut register_state) { @@ -353,7 +393,6 @@ pub async fn run_ui() -> Result<()> { } } - let mut should_exit = false; match event_outcome_result { Ok(outcome) => match outcome { @@ -383,68 +422,86 @@ pub async fn run_ui() -> Result<()> { } } - - - let position_changed = app_state.current_position != position_before_event; - let current_total_count = app_state.total_count; +// --- MODIFIED: Position Change Handling (operates on form_state) --- + let position_changed = form_state.current_position != position_before_event; let mut position_logic_needs_redraw = false; - if app_state.ui.show_form { + if app_state.ui.show_form { // Only if the form is active if position_changed && !event_handler.is_edit_mode { - let current_input = form_state.get_current_input(); - let max_cursor_pos = if !current_input.is_empty() { current_input.len() - 1 } else { 0 }; - form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos); + // This part is okay: update cursor for the current field BEFORE loading new data + let current_input_before_load = form_state.get_current_input(); + let max_cursor_pos_before_load = if !current_input_before_load.is_empty() { current_input_before_load.chars().count() } else { 0 }; + form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos_before_load); position_logic_needs_redraw = true; - if app_state.current_position > current_total_count + 1 { - app_state.current_position = current_total_count + 1; + // Validate new form_state.current_position + if form_state.total_count > 0 && form_state.current_position > form_state.total_count + 1 { + form_state.current_position = form_state.total_count + 1; // Cap at new entry + } else if form_state.total_count == 0 && form_state.current_position > 1 { + form_state.current_position = 1; // Cap at new entry for empty table + } + if form_state.current_position == 0 && form_state.total_count > 0 { + form_state.current_position = 1; // Don't allow 0 if there are records } - if app_state.current_position > current_total_count { - form_state.reset_to_empty(); - form_state.current_field = 0; - } else if app_state.current_position >= 1 && app_state.current_position <= current_total_count { - let current_position_to_load = app_state.current_position; - let load_message = UiService::load_adresar_by_position( - &mut grpc_client, - &mut app_state, - &mut form_state, - current_position_to_load, - ) - .await.with_context(|| format!("Failed to load adresar by position: {}", current_position_to_load))?; - let current_input_after_load = form_state.get_current_input(); - let max_cursor_pos_after_load = if !event_handler.is_edit_mode && !current_input_after_load.is_empty() { - current_input_after_load.len() - 1 - } else { - current_input_after_load.len() - }; - form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos_after_load); - - if !load_message.starts_with("Loaded entry") || event_handler.command_message.is_empty() { - event_handler.command_message = load_message; + // Load data for the new position OR reset for new entry + if (form_state.total_count > 0 && form_state.current_position <= form_state.total_count && form_state.current_position > 0) + { + // It's an existing record position + match UiService::load_table_data_by_position(&mut grpc_client, &mut form_state).await { + Ok(load_message) => { + if event_handler.command_message.is_empty() || !load_message.starts_with("Error") { + event_handler.command_message = load_message; + } + } + Err(e) => { + event_handler.command_message = format!("Error loading data: {}", e); + // Consider what to do with form_state here - maybe revert position or clear form + } } } else { - app_state.current_position = 1.min(current_total_count + 1); - if app_state.current_position > current_total_count { - form_state.reset_to_empty(); - form_state.current_field = 0; - } - + // Position indicates a new entry (or table is empty and position is 1) + form_state.reset_to_empty(); // This sets id=0, clears values, and sets current_position correctly + event_handler.command_message = format!("New entry for {}.{}", form_state.profile_name, form_state.table_name); } - } else if !position_changed && !event_handler.is_edit_mode { - let current_input = form_state.get_current_input(); - let max_cursor_pos = if !current_input.is_empty() { current_input.len() - 1 } else { 0 }; + + // NOW, after data is loaded or form is reset, get the current input string and its length + let current_input_after_load_str = form_state.get_current_input(); + let current_input_len_after_load = current_input_after_load_str.chars().count(); + + let max_cursor_pos_for_readonly_after_load = if current_input_len_after_load > 0 { + current_input_len_after_load.saturating_sub(1) + } else { + 0 + }; + + if event_handler.is_edit_mode { + form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(current_input_len_after_load); + } else { + form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos_for_readonly_after_load); + // The check for empty string is implicitly handled by max_cursor_pos_for_readonly_after_load being 0 + } + + } else if !position_changed && !event_handler.is_edit_mode && app_state.ui.show_form { + // Update cursor if not editing and position didn't change (e.g. arrow keys within field) + let current_input_str = form_state.get_current_input(); + let current_input_len = current_input_str.chars().count(); + let max_cursor_pos = if current_input_len > 0 { + current_input_len.saturating_sub(1) + } else { + 0 + }; form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos); } } else if app_state.ui.show_register { - if !event_handler.is_edit_mode { + if !event_handler.is_edit_mode { let current_input = register_state.get_current_input(); let max_cursor_pos = if !current_input.is_empty() { current_input.len() - 1 } else { 0 }; register_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos); } } else if app_state.ui.show_login { - if !event_handler.is_edit_mode { + if !event_handler.is_edit_mode { let current_input = login_state.get_current_input(); let max_cursor_pos = if !current_input.is_empty() { current_input.len() - 1 } else { 0 }; login_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos); @@ -455,12 +512,10 @@ pub async fn run_ui() -> Result<()> { needs_redraw = true; } - if should_exit { return Ok(()); } - let now = Instant::now(); let frame_duration = now.duration_since(last_frame_time); last_frame_time = now;