BREAKING CHANGES updating gRPC based on the enum now

This commit is contained in:
filipriec
2025-04-07 21:27:01 +02:00
parent bb103fac6c
commit 70678432c6
7 changed files with 316 additions and 182 deletions

View File

@@ -4,6 +4,8 @@ use crate::services::grpc_client::GrpcClient;
use crate::state::canvas_state::CanvasState; use crate::state::canvas_state::CanvasState;
use crate::state::pages::form::FormState; use crate::state::pages::form::FormState;
use crate::tui::functions::common::form::{revert, save}; use crate::tui::functions::common::form::{revert, save};
use crate::tui::functions::common::form::SaveOutcome;
use crate::modes::handlers::event::EventOutcome;
use crossterm::event::{KeyCode, KeyEvent}; use crossterm::event::{KeyCode, KeyEvent};
use std::any::Any; use std::any::Any;
@@ -11,48 +13,61 @@ pub async fn execute_common_action<S: CanvasState + Any>(
action: &str, action: &str,
state: &mut S, state: &mut S,
grpc_client: &mut GrpcClient, grpc_client: &mut GrpcClient,
is_saved: &mut bool,
current_position: &mut u64, current_position: &mut u64,
total_count: u64, total_count: u64,
) -> Result<String, Box<dyn std::error::Error>> { ) -> Result<EventOutcome, Box<dyn std::error::Error>> {
match action { match action {
"save" | "revert" => { "save" | "revert" => {
if !state.has_unsaved_changes() { if !state.has_unsaved_changes() {
return Ok("No changes to save or revert.".to_string()); return Ok(EventOutcome::Ok("No changes to save or revert.".to_string()));
} }
if let Some(form_state) = if let Some(form_state) =
(state as &mut dyn Any).downcast_mut::<FormState>() (state as &mut dyn Any).downcast_mut::<FormState>()
{ {
match action { match action {
"save" => { "save" => {
save( let save_result = save(
form_state, form_state,
grpc_client, grpc_client,
is_saved,
current_position, current_position,
total_count, total_count,
) ).await;
.await
match save_result {
Ok(save_outcome) => {
let message = match save_outcome {
SaveOutcome::NoChange => "No changes to save.".to_string(),
SaveOutcome::UpdatedExisting => "Entry updated.".to_string(),
SaveOutcome::CreatedNew(_) => "New entry created.".to_string(),
};
Ok(EventOutcome::DataSaved(save_outcome, message))
}
Err(e) => Err(e),
}
} }
"revert" => { "revert" => {
revert( let revert_result = revert(
form_state, form_state,
grpc_client, grpc_client,
current_position, current_position,
total_count, total_count,
) ).await;
.await
match revert_result {
Ok(message) => Ok(EventOutcome::Ok(message)),
Err(e) => Err(e),
}
} }
_ => unreachable!(), _ => unreachable!(),
} }
} else { } else {
Ok(format!( Ok(EventOutcome::Ok(format!(
"Action '{}' not implemented for this state type.", "Action '{}' not implemented for this state type.",
action action
)) )))
} }
} }
_ => Ok(format!("Common action '{}' not handled here.", action)), _ => Ok(EventOutcome::Ok(format!("Common action '{}' not handled here.", action))),
} }
} }
@@ -61,10 +76,10 @@ pub async fn execute_edit_action<S: CanvasState>(
key: KeyEvent, key: KeyEvent,
state: &mut S, state: &mut S,
ideal_cursor_column: &mut usize, ideal_cursor_column: &mut usize,
_grpc_client: &mut GrpcClient, grpc_client: &mut GrpcClient,
_is_saved: &mut bool, is_saved: &mut bool,
_current_position: &mut u64, current_position: &mut u64,
_total_count: u64, total_count: u64,
) -> Result<String, Box<dyn std::error::Error>> { ) -> Result<String, Box<dyn std::error::Error>> {
match action { match action {
"insert_char" => { "insert_char" => {

View File

@@ -1,10 +1,12 @@
// src/modes/canvas/common.rs // src/modes/canvas/common_mode.rs
use crate::tui::terminal::core::TerminalCore; use crate::tui::terminal::core::TerminalCore;
use crate::state::pages::{form::FormState, auth::AuthState}; use crate::state::pages::{form::FormState, auth::AuthState};
use crate::state::state::AppState; use crate::state::state::AppState;
use crate::services::grpc_client::GrpcClient; use crate::services::grpc_client::GrpcClient;
use crate::services::auth::AuthClient; use crate::services::auth::AuthClient;
use crate::modes::handlers::event::EventOutcome;
use crate::tui::functions::common::form::SaveOutcome;
use crate::tui::functions::common::{ use crate::tui::functions::common::{
form::{save as form_save, revert as form_revert}, form::{save as form_save, revert as form_revert},
login::{save as login_save, revert as login_revert} login::{save as login_save, revert as login_revert}
@@ -20,46 +22,54 @@ pub async fn handle_core_action(
app_state: &mut AppState, app_state: &mut AppState,
current_position: &mut u64, current_position: &mut u64,
total_count: u64, total_count: u64,
) -> Result<(bool, String), Box<dyn std::error::Error>> { ) -> Result<EventOutcome, Box<dyn std::error::Error>> {
match action { match action {
"save" => { "save" => {
if app_state.ui.show_login { if app_state.ui.show_login {
let message = login_save(auth_state, auth_client, app_state).await?; let message = login_save(auth_state, auth_client, app_state).await?;
Ok((false, message)) Ok(EventOutcome::Ok(message))
} else { } else {
let message = form_save( let save_outcome = form_save(
form_state, form_state,
grpc_client, grpc_client,
&mut app_state.ui.is_saved,
current_position, current_position,
total_count, total_count,
).await?; ).await?;
Ok((false, message)) 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" => { "force_quit" => {
terminal.cleanup()?; terminal.cleanup()?;
Ok((true, "Force exiting without saving.".to_string())) Ok(EventOutcome::Exit("Force exiting without saving.".to_string()))
}, },
"save_and_quit" => { "save_and_quit" => {
let message = if app_state.ui.show_login { let message = if app_state.ui.show_login {
login_save(auth_state, auth_client, app_state).await? login_save(auth_state, auth_client, app_state).await?
} else { } else {
form_save( let save_outcome = form_save(
form_state, form_state,
grpc_client, grpc_client,
&mut app_state.ui.is_saved,
current_position, current_position,
total_count, total_count,
).await? ).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()?; terminal.cleanup()?;
Ok((true, format!("{}. Exiting application.", message))) Ok(EventOutcome::Exit(format!("{}. Exiting application.", message)))
}, },
"revert" => { "revert" => {
if app_state.ui.show_login { if app_state.ui.show_login {
let message = login_revert(auth_state, app_state).await; let message = login_revert(auth_state, app_state).await;
Ok((false, message)) Ok(EventOutcome::Ok(message))
} else { } else {
let message = form_revert( let message = form_revert(
form_state, form_state,
@@ -67,9 +77,9 @@ pub async fn handle_core_action(
current_position, current_position,
total_count, total_count,
).await?; ).await?;
Ok((false, message)) Ok(EventOutcome::Ok(message))
} }
}, },
_ => Ok((false, format!("Core action not handled: {}", action))), _ => Ok(EventOutcome::Ok(format!("Core action not handled: {}", action))),
} }
} }

View File

@@ -1,4 +1,4 @@
// src/modes/handlers/command_mode.rs // src/modes/common/command_mode.rs
use crossterm::event::{KeyEvent, KeyCode, KeyModifiers}; use crossterm::event::{KeyEvent, KeyCode, KeyModifiers};
use crate::config::binds::config::Config; use crate::config::binds::config::Config;
@@ -7,6 +7,14 @@ use crate::state::pages::form::FormState;
use crate::modes::common::commands::CommandHandler; use crate::modes::common::commands::CommandHandler;
use crate::tui::terminal::core::TerminalCore; use crate::tui::terminal::core::TerminalCore;
use crate::tui::functions::common::form::{save, revert}; use crate::tui::functions::common::form::{save, revert};
use crate::modes::handlers::event::EventOutcome;
use crate::tui::functions::common::form::SaveOutcome;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CommandOutcome {
ExitCommandMode(String),
// Other command-specific outcomes if needed
}
pub async fn handle_command_event( pub async fn handle_command_event(
key: KeyEvent, key: KeyEvent,
@@ -19,15 +27,12 @@ pub async fn handle_command_event(
terminal: &mut TerminalCore, terminal: &mut TerminalCore,
current_position: &mut u64, current_position: &mut u64,
total_count: u64, total_count: u64,
) -> Result<(bool, String, bool), Box<dyn std::error::Error>> { ) -> Result<EventOutcome, Box<dyn std::error::Error>> {
// Return value: (should_exit, message, should_exit_command_mode)
// Exit command mode (via configurable keybinding) // Exit command mode (via configurable keybinding)
if config.is_exit_command_mode(key.code, key.modifiers) { if config.is_exit_command_mode(key.code, key.modifiers) {
command_input.clear(); command_input.clear();
*command_message = "".to_string(); *command_message = "".to_string();
return Ok((false, "".to_string(), true)); return Ok(EventOutcome::Ok("Exited command mode".to_string()));
} }
// Execute command (via configurable keybinding, defaults to Enter) // Execute command (via configurable keybinding, defaults to Enter)
@@ -48,7 +53,7 @@ pub async fn handle_command_event(
// Backspace (via configurable keybinding, defaults to Backspace) // Backspace (via configurable keybinding, defaults to Backspace)
if config.is_command_backspace(key.code, key.modifiers) { if config.is_command_backspace(key.code, key.modifiers) {
command_input.pop(); command_input.pop();
return Ok((false, "".to_string(), false)); return Ok(EventOutcome::Ok("".to_string()));
} }
// Regular character input - accept any character in command mode // Regular character input - accept any character in command mode
@@ -56,12 +61,12 @@ pub async fn handle_command_event(
// Accept regular or shifted characters (e.g., 'a' or 'A') // Accept regular or shifted characters (e.g., 'a' or 'A')
if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT { if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT {
command_input.push(c); command_input.push(c);
return Ok((false, "".to_string(), false)); return Ok(EventOutcome::Ok("".to_string()));
} }
} }
// Ignore all other keys // Ignore all other keys
Ok((false, "".to_string(), false)) Ok(EventOutcome::Ok("".to_string()))
} }
async fn process_command( async fn process_command(
@@ -74,12 +79,12 @@ async fn process_command(
terminal: &mut TerminalCore, terminal: &mut TerminalCore,
current_position: &mut u64, current_position: &mut u64,
total_count: u64, total_count: u64,
) -> Result<(bool, String, bool), Box<dyn std::error::Error>> { ) -> Result<EventOutcome, Box<dyn std::error::Error>> {
// Clone the trimmed command to avoid borrow issues // Clone the trimmed command to avoid borrow issues
let command = command_input.trim().to_string(); let command = command_input.trim().to_string();
if command.is_empty() { if command.is_empty() {
*command_message = "Empty command".to_string(); *command_message = "Empty command".to_string();
return Ok((false, command_message.clone(), false)); return Ok(EventOutcome::Ok(command_message.clone()));
} }
// Get the action for the command (now checks global and common bindings too) // Get the action for the command (now checks global and common bindings too)
@@ -92,18 +97,26 @@ async fn process_command(
.handle_command(action, terminal) .handle_command(action, terminal)
.await?; .await?;
command_input.clear(); command_input.clear();
Ok((should_exit, message, true)) if should_exit {
Ok(EventOutcome::Exit(message))
} else {
Ok(EventOutcome::Ok(message))
}
}, },
"save" => { "save" => {
let message = save( let outcome = save(
form_state, form_state,
grpc_client, grpc_client,
&mut command_handler.is_saved,
current_position, current_position,
total_count, total_count,
).await?; ).await?;
let message = match outcome {
SaveOutcome::CreatedNew(_) => "New entry created".to_string(),
SaveOutcome::UpdatedExisting => "Entry updated".to_string(),
SaveOutcome::NoChange => "No changes to save".to_string(),
};
command_input.clear(); command_input.clear();
return Ok((false, message, true)); Ok(EventOutcome::DataSaved(outcome, message))
}, },
"revert" => { "revert" => {
let message = revert( let message = revert(
@@ -113,17 +126,12 @@ async fn process_command(
total_count, total_count,
).await?; ).await?;
command_input.clear(); command_input.clear();
return Ok((false, message, true)); Ok(EventOutcome::Ok(message))
},
"unknown" => {
let message = format!("Unknown command: {}", command);
command_input.clear();
return Ok((false, message, true));
}, },
_ => { _ => {
let message = format!("Unhandled action: {}", action); let message = format!("Unhandled action: {}", action);
command_input.clear(); command_input.clear();
return Ok((false, message, true)); Ok(EventOutcome::Ok(message))
} }
} }
} }

View File

@@ -17,6 +17,15 @@ use crate::modes::{
}; };
use crate::config::binds::key_sequences::KeySequenceTracker; use crate::config::binds::key_sequences::KeySequenceTracker;
use crate::modes::handlers::mode_manager::{ModeManager, AppMode}; use crate::modes::handlers::mode_manager::{ModeManager, AppMode};
use crate::tui::functions::common::form::SaveOutcome;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EventOutcome {
Ok(String), // Normal operation, display message
Exit(String), // Signal app exit, display message
DataSaved(SaveOutcome, String), // Data save attempted, include outcome and message
// Add other outcomes like QuitRequested, SaveAndQuitRequested later if needed
}
pub struct EventHandler { pub struct EventHandler {
pub command_mode: bool, pub command_mode: bool,
@@ -55,7 +64,7 @@ impl EventHandler {
app_state: &mut crate::state::state::AppState, app_state: &mut crate::state::state::AppState,
total_count: u64, total_count: u64,
current_position: &mut u64, current_position: &mut u64,
) -> Result<(bool, String), Box<dyn std::error::Error>> { ) -> Result<EventOutcome, Box<dyn std::error::Error>> {
let current_mode = ModeManager::derive_mode(app_state, self); let current_mode = ModeManager::derive_mode(app_state, self);
app_state.update_mode(current_mode); app_state.update_mode(current_mode);
@@ -64,9 +73,10 @@ impl EventHandler {
let modifiers = key.modifiers; let modifiers = key.modifiers;
if UiStateHandler::toggle_sidebar(&mut app_state.ui, config, key_code, modifiers) { if UiStateHandler::toggle_sidebar(&mut app_state.ui, config, key_code, modifiers) {
return Ok((false, format!("Sidebar {}", let message = format!("Sidebar {}",
if app_state.ui.show_sidebar { "shown" } else { "hidden" } if app_state.ui.show_sidebar { "shown" } else { "hidden" }
))); );
return Ok(EventOutcome::Ok(message));
} }
match current_mode { match current_mode {
@@ -90,7 +100,7 @@ impl EventHandler {
self.edit_mode_cooldown = true; self.edit_mode_cooldown = true;
self.command_message = "Edit mode".to_string(); self.command_message = "Edit mode".to_string();
terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?; terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?;
return Ok((false, self.command_message.clone())); return Ok(EventOutcome::Ok(self.command_message.clone()));
} }
if config.is_enter_edit_mode_after(key_code, modifiers) && if config.is_enter_edit_mode_after(key_code, modifiers) &&
@@ -119,7 +129,7 @@ impl EventHandler {
self.edit_mode_cooldown = true; self.edit_mode_cooldown = true;
self.command_message = "Edit mode (after cursor)".to_string(); self.command_message = "Edit mode (after cursor)".to_string();
terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?; terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?;
return Ok((false, self.command_message.clone())); return Ok(EventOutcome::Ok(self.command_message.clone()));
} }
if let Some(action) = config.get_read_only_action_for_key(key_code, modifiers) { if let Some(action) = config.get_read_only_action_for_key(key_code, modifiers) {
@@ -127,7 +137,7 @@ impl EventHandler {
self.command_mode = true; self.command_mode = true;
self.command_input.clear(); self.command_input.clear();
self.command_message.clear(); self.command_message.clear();
return Ok((false, String::new())); return Ok(EventOutcome::Ok(String::new()));
} }
} }
@@ -154,7 +164,7 @@ impl EventHandler {
} }
} }
return read_only::handle_read_only_event( let message = read_only::handle_read_only_event(
app_state, app_state,
key, key,
config, config,
@@ -167,26 +177,27 @@ impl EventHandler {
&mut self.command_message, &mut self.command_message,
&mut self.edit_mode_cooldown, &mut self.edit_mode_cooldown,
&mut self.ideal_cursor_column, &mut self.ideal_cursor_column,
).await; ).await?;
return Ok(EventOutcome::Ok(message));
}, },
AppMode::Edit => { AppMode::Edit => {
if config.is_exit_edit_mode(key_code, modifiers) { if config.is_exit_edit_mode(key_code, modifiers) {
self.is_edit_mode = false; self.is_edit_mode = false;
self.edit_mode_cooldown = true; self.edit_mode_cooldown = true;
let has_changes = if app_state.ui.show_login { let has_changes = if app_state.ui.show_login {
auth_state.has_unsaved_changes() auth_state.has_unsaved_changes()
} else { } else {
form_state.has_unsaved_changes() form_state.has_unsaved_changes()
}; };
self.command_message = if has_changes { self.command_message = if has_changes {
"Exited edit mode (unsaved changes remain)".to_string() "Exited edit mode (unsaved changes remain)".to_string()
} else { } else {
"Read-only mode".to_string() "Read-only mode".to_string()
}; };
terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?; terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?;
let current_input = if app_state.ui.show_login { let current_input = if app_state.ui.show_login {
@@ -210,7 +221,7 @@ impl EventHandler {
self.ideal_cursor_column = form_state.current_cursor_pos(); self.ideal_cursor_column = form_state.current_cursor_pos();
} }
} }
return Ok((false, self.command_message.clone())); return Ok(EventOutcome::Ok(self.command_message.clone()));
} }
if let Some(action) = config.get_action_for_key_in_mode( if let Some(action) = config.get_action_for_key_in_mode(
@@ -220,23 +231,23 @@ impl EventHandler {
) { ) {
match action { match action {
"save" | "force_quit" | "save_and_quit" | "revert" => { "save" | "force_quit" | "save_and_quit" | "revert" => {
return common_mode::handle_core_action( return common_mode::handle_core_action(
action, action,
form_state, form_state,
auth_state, auth_state,
grpc_client, grpc_client,
&mut self.auth_client, &mut self.auth_client,
terminal, terminal,
app_state, app_state,
current_position, current_position,
total_count, total_count,
).await; ).await;
}, },
_ => {} _ => {}
} }
} }
let result = edit::handle_edit_event( let message = edit::handle_edit_event(
app_state.ui.show_login, app_state.ui.show_login,
key, key,
config, config,
@@ -244,18 +255,17 @@ impl EventHandler {
auth_state, auth_state,
&mut self.ideal_cursor_column, &mut self.ideal_cursor_column,
&mut self.command_message, &mut self.command_message,
&mut app_state.ui.is_saved,
current_position, current_position,
total_count, total_count,
grpc_client, grpc_client,
).await?; ).await?;
self.key_sequence_tracker.reset(); self.key_sequence_tracker.reset();
return Ok((false, result)); return Ok(EventOutcome::Ok(message));
}, },
AppMode::Command => { AppMode::Command => {
let (should_exit, message, exit_command_mode) = command_mode::handle_command_event( let outcome = command_mode::handle_command_event(
key, key,
config, config,
form_state, form_state,
@@ -267,17 +277,18 @@ impl EventHandler {
current_position, current_position,
total_count, total_count,
).await?; ).await?;
if exit_command_mode { if let EventOutcome::Ok(msg) = &outcome {
self.command_mode = false; if msg == "Exited command mode" {
self.command_mode = false;
}
} }
return Ok(outcome);
return Ok((should_exit, message));
} }
} }
} }
self.edit_mode_cooldown = false; self.edit_mode_cooldown = false;
Ok((false, self.command_message.clone())) Ok(EventOutcome::Ok(self.command_message.clone()))
} }
} }

View File

@@ -2,6 +2,7 @@
use crate::services::grpc_client::GrpcClient; use crate::services::grpc_client::GrpcClient;
use crate::state::pages::form::FormState; use crate::state::pages::form::FormState;
use crate::tui::functions::common::form::SaveOutcome;
use crate::state::state::AppState; use crate::state::state::AppState;
pub struct UiService; pub struct UiService;
@@ -85,5 +86,27 @@ impl UiService {
} }
} }
} }
/// Handles the consequences of a save operation, like updating counts.
pub async fn handle_save_outcome(
save_outcome: SaveOutcome,
grpc_client: &mut GrpcClient,
app_state: &mut AppState,
form_state: &mut FormState, // Needed to potentially update position/ID
) -> Result<(), Box<dyn std::error::Error>> {
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)
}
SaveOutcome::UpdatedExisting | SaveOutcome::NoChange => {
// No count update needed for these outcomes
}
}
Ok(())
}
} }

View File

@@ -4,17 +4,26 @@ use crate::services::grpc_client::GrpcClient;
use crate::state::pages::form::FormState; use crate::state::pages::form::FormState;
use common::proto::multieko2::adresar::{PostAdresarRequest, PutAdresarRequest}; use common::proto::multieko2::adresar::{PostAdresarRequest, PutAdresarRequest};
#[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)
}
/// Shared logic for saving the current form state /// Shared logic for saving the current form state
pub async fn save( pub async fn save(
form_state: &mut FormState, form_state: &mut FormState,
grpc_client: &mut GrpcClient, grpc_client: &mut GrpcClient,
is_saved: &mut bool,
current_position: &mut u64, current_position: &mut u64,
total_count: u64, total_count: u64,
) -> Result<String, Box<dyn std::error::Error>> { ) -> Result<SaveOutcome, Box<dyn std::error::Error>> { // <-- Return SaveOutcome
if !form_state.has_unsaved_changes {
return Ok(SaveOutcome::NoChange); // Early exit if no changes
}
let is_new = *current_position == total_count + 1; let is_new = *current_position == total_count + 1;
let message = if is_new { let outcome = if is_new {
let post_request = PostAdresarRequest { let post_request = PostAdresarRequest {
firma: form_state.values[0].clone(), firma: form_state.values[0].clone(),
kz: form_state.values[1].clone(), kz: form_state.values[1].clone(),
@@ -33,10 +42,9 @@ pub async fn save(
fax: form_state.values[14].clone(), fax: form_state.values[14].clone(),
}; };
let response = grpc_client.post_adresar(post_request).await?; let response = grpc_client.post_adresar(post_request).await?;
let new_total = grpc_client.get_adresar_count().await?; let new_id = response.into_inner().id;
*current_position = new_total; form_state.id = new_id;
form_state.id = response.into_inner().id; SaveOutcome::CreatedNew(new_id) // <-- Return CreatedNew with ID
"New entry created".to_string()
} else { } else {
let put_request = PutAdresarRequest { let put_request = PutAdresarRequest {
id: form_state.id, id: form_state.id,
@@ -57,12 +65,11 @@ pub async fn save(
fax: form_state.values[14].clone(), fax: form_state.values[14].clone(),
}; };
let _ = grpc_client.put_adresar(put_request).await?; let _ = grpc_client.put_adresar(put_request).await?;
"Entry updated".to_string() SaveOutcome::UpdatedExisting
}; };
*is_saved = true;
form_state.has_unsaved_changes = false; form_state.has_unsaved_changes = false;
Ok(message) Ok(outcome)
} }
/// Discard changes since last save /// Discard changes since last save

View File

@@ -1,26 +1,25 @@
// src/ui/handlers/ui.rs // src/ui/handlers/ui.rs
use crate::tui::terminal::TerminalCore; use crate::config::binds::config::Config;
use crate::config::colors::themes::Theme;
use crate::modes::common::commands::CommandHandler;
use crate::modes::handlers::event::{EventHandler, EventOutcome}; // Import EventOutcome
use crate::modes::handlers::mode_manager::{AppMode, ModeManager};
use crate::services::grpc_client::GrpcClient; use crate::services::grpc_client::GrpcClient;
use crate::services::ui_service::UiService; use crate::services::ui_service::UiService;
use crate::tui::terminal::EventReader;
use crate::modes::common::commands::CommandHandler;
use crate::modes::handlers::mode_manager::{AppMode, ModeManager};
use crate::config::colors::themes::Theme;
use crate::config::binds::config::Config;
use crate::ui::handlers::render::render_ui;
use crate::state::pages::form::FormState;
use crate::state::pages::auth::AuthState;
use crate::state::canvas_state::CanvasState; use crate::state::canvas_state::CanvasState;
use crate::modes::handlers::event::EventHandler; use crate::state::pages::auth::AuthState;
use crate::state::pages::form::FormState;
use crate::state::state::AppState; use crate::state::state::AppState;
use crate::tui::functions::common::form::SaveOutcome; // Import SaveOutcome
use crate::tui::terminal::{EventReader, TerminalCore};
use crate::ui::handlers::render::render_ui;
use crossterm::cursor::SetCursorStyle; use crossterm::cursor::SetCursorStyle;
pub async fn run_ui() -> Result<(), Box<dyn std::error::Error>> { pub async fn run_ui() -> Result<(), Box<dyn std::error::Error>> {
let config = Config::load()?; let config = Config::load()?;
let mut terminal = TerminalCore::new()?; let mut terminal = TerminalCore::new()?;
let mut grpc_client = GrpcClient::new().await?; let mut grpc_client = GrpcClient::new().await?;
// let auth_client = AuthClient::new().await?; // AuthClient is now inside EventHandler
let mut command_handler = CommandHandler::new(); let mut command_handler = CommandHandler::new();
let theme = Theme::from_str(&config.colors.theme); let theme = Theme::from_str(&config.colors.theme);
let mut auth_state = AuthState::default(); // The single source of truth for AuthState let mut auth_state = AuthState::default(); // The single source of truth for AuthState
@@ -29,7 +28,9 @@ pub async fn run_ui() -> Result<(), Box<dyn std::error::Error>> {
let mut app_state = AppState::new()?; let mut app_state = AppState::new()?;
// Initialize app state with profile tree and table structure // Initialize app state with profile tree and table structure
let column_names = UiService::initialize_app_state(&mut grpc_client, &mut app_state).await?; let column_names =
UiService::initialize_app_state(&mut grpc_client, &mut app_state)
.await?;
// Initialize FormState with dynamic fields // Initialize FormState with dynamic fields
let mut form_state = FormState::new(column_names); let mut form_state = FormState::new(column_names);
@@ -39,7 +40,8 @@ pub async fn run_ui() -> Result<(), Box<dyn std::error::Error>> {
let event_reader = EventReader::new(); let event_reader = EventReader::new();
// Fetch the total count of Adresar entries // Fetch the total count of Adresar entries
UiService::initialize_adresar_count(&mut grpc_client, &mut app_state).await?; UiService::initialize_adresar_count(&mut grpc_client, &mut app_state)
.await?;
form_state.reset_to_empty(); form_state.reset_to_empty();
loop { loop {
@@ -63,97 +65,158 @@ pub async fn run_ui() -> Result<(), Box<dyn std::error::Error>> {
); );
})?; })?;
// --- Cursor Visibility Logic --- // --- Cursor Visibility Logic ---
let current_mode = ModeManager::derive_mode(&app_state, &event_handler); let current_mode = ModeManager::derive_mode(&app_state, &event_handler);
match current_mode { match current_mode {
AppMode::Edit => { AppMode::Edit => {
terminal.show_cursor()?; terminal.show_cursor()?;
} }
AppMode::ReadOnly => { AppMode::ReadOnly => {
if !app_state.ui.focus_outside_canvas { if !app_state.ui.focus_outside_canvas {
terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?; terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?;
} else { } else {
terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; terminal
} .set_cursor_style(SetCursorStyle::SteadyUnderScore)?;
terminal.show_cursor()?; // Ensure visible }
} terminal.show_cursor()?; // Ensure visible
AppMode::General | AppMode::Command => { }
terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; AppMode::General | AppMode::Command => {
terminal.show_cursor()?; // Ensure visible (though might not be positioned meaningfully) terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?;
} terminal.show_cursor()?; // Ensure visible (though might not be positioned meaningfully)
} }
// --- End Cursor Visibility Logic --- }
// --- End Cursor Visibility Logic ---
let total_count = app_state.total_count;
let total_count = app_state.total_count; // Keep track for save logic
let mut current_position = app_state.current_position; let mut current_position = app_state.current_position;
// Store position before event handling to detect navigation
let position_before_event = current_position; let position_before_event = current_position;
let event = event_reader.read_event()?; let event = event_reader.read_event()?;
let (should_exit, message) = event_handler.handle_event(
event,
&config,
&mut terminal,
&mut grpc_client,
&mut command_handler,
&mut form_state,
&mut auth_state, // Pass the single AuthState instance here too
&mut app_state,
total_count,
&mut current_position,
).await?;
// Get the outcome from the event handler
let event_outcome_result = event_handler
.handle_event(
event,
&config,
&mut terminal, // Pass terminal mutably
&mut grpc_client,
&mut command_handler,
&mut form_state,
&mut auth_state,
&mut app_state,
total_count, // Pass the count *before* potential save
&mut current_position,
)
.await;
// Update position based on handler's modification
app_state.current_position = current_position; app_state.current_position = current_position;
let position_changed = app_state.current_position != position_before_event; // --- Centralized Consequence Handling ---
let mut should_exit = false;
match event_outcome_result {
// Handle the Result first
Ok(outcome) => match outcome {
// Handle the Ok variant containing EventOutcome
EventOutcome::Ok(message) => {
if !message.is_empty() {
event_handler.command_message = message;
}
}
EventOutcome::Exit(message) => {
event_handler.command_message = message;
should_exit = true;
}
EventOutcome::DataSaved(save_outcome, message) => {
event_handler.command_message = message; // Show save status
// *** Delegate outcome handling to UiService ***
if let Err(e) = UiService::handle_save_outcome(
save_outcome,
&mut grpc_client,
&mut app_state,
&mut form_state,
)
.await
{
// Handle potential errors from the outcome handler itself
event_handler.command_message =
format!("Error handling save outcome: {}", e);
}
// No count update needed for UpdatedExisting or NoChange
}
},
Err(e) => {
// Handle errors from handle_event, e.g., log or display
event_handler.command_message = format!("Error: {}", e);
// Decide if the error is fatal, maybe set should_exit = true;
}
}
// --- Position Change Handling (after outcome processing) ---
let position_changed =
app_state.current_position != position_before_event; // Calculate after potential update
// Recalculate total_count *after* potential update
let current_total_count = app_state.total_count;
// Handle position changes and update form state (Only when form is shown) // Handle position changes and update form state (Only when form is shown)
if app_state.ui.show_form { if app_state.ui.show_form {
if position_changed && !event_handler.is_edit_mode { if position_changed && !event_handler.is_edit_mode {
let current_input = form_state.get_current_input(); let current_input = form_state.get_current_input();
let max_cursor_pos = if !current_input.is_empty() { let max_cursor_pos = if !current_input.is_empty() {
current_input.len() - 1 // Limit to last character in readonly mode current_input.len() - 1 // Limit to last character in readonly mode
} else { } else {
0 0
}; };
form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos); form_state.current_cursor_pos =
event_handler.ideal_cursor_column.min(max_cursor_pos);
// Ensure position never exceeds total_count + 1 // Ensure position never exceeds total_count + 1
if app_state.current_position > total_count + 1 { if app_state.current_position > current_total_count + 1 {
app_state.current_position = total_count + 1; app_state.current_position = current_total_count + 1;
} }
if app_state.current_position > total_count { if app_state.current_position > current_total_count {
// New entry - reset form // New entry - reset form
form_state.reset_to_empty(); form_state.reset_to_empty();
form_state.current_field = 0; form_state.current_field = 0;
} else if app_state.current_position >= 1 && app_state.current_position <= total_count { } else if app_state.current_position >= 1
&& app_state.current_position <= current_total_count
{
// Existing entry - load data // Existing entry - load data
let current_position_to_load = app_state.current_position; // Use a copy let current_position_to_load = app_state.current_position; // Use a copy
let load_message = UiService::load_adresar_by_position( let load_message = UiService::load_adresar_by_position(
&mut grpc_client, &mut grpc_client,
&mut app_state, // Pass app_state mutably if needed by the service &mut app_state, // Pass app_state mutably if needed by the service
&mut form_state, &mut form_state,
current_position_to_load current_position_to_load,
).await?; )
.await?;
let current_input = form_state.get_current_input(); let current_input = form_state.get_current_input();
let max_cursor_pos = if !event_handler.is_edit_mode && !current_input.is_empty() { let max_cursor_pos = if !event_handler.is_edit_mode
current_input.len() - 1 // In readonly mode, limit to last character && !current_input.is_empty()
{
current_input.len() - 1 // In readonly mode, limit to last character
} else { } else {
current_input.len() current_input.len()
}; };
form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos); form_state.current_cursor_pos = event_handler
.ideal_cursor_column
.min(max_cursor_pos);
// Don't overwrite message from handle_event if load_message is simple success // Don't overwrite message from handle_event if load_message is simple success
if !load_message.starts_with("Loaded entry") || message.is_empty() { if !load_message.starts_with("Loaded entry")
event_handler.command_message = load_message; || event_handler.command_message.is_empty()
{
event_handler.command_message = load_message;
} }
} else { } else {
// Invalid position (e.g., 0) - reset to first entry or new entry mode // Invalid position (e.g., 0) - reset to first entry or new entry mode
app_state.current_position = 1.min(total_count + 1); // Go to 1 or new entry if empty app_state.current_position =
if app_state.current_position > total_count { 1.min(current_total_count + 1); // Go to 1 or new entry if empty
form_state.reset_to_empty(); if app_state.current_position > total_count {
form_state.current_field = 0; form_state.reset_to_empty();
} form_state.current_field = 0;
}
} }
} else if !position_changed && !event_handler.is_edit_mode { } else if !position_changed && !event_handler.is_edit_mode {
// If position didn't change but we are in read-only, just adjust cursor // If position didn't change but we are in read-only, just adjust cursor
@@ -163,29 +226,26 @@ pub async fn run_ui() -> Result<(), Box<dyn std::error::Error>> {
} else { } else {
0 0
}; };
form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos); form_state.current_cursor_pos =
event_handler.ideal_cursor_column.min(max_cursor_pos);
} }
} else if app_state.ui.show_login { } else if app_state.ui.show_login {
// Handle cursor updates for AuthState if needed, similar to FormState // Handle cursor updates for AuthState if needed, similar to FormState
if !event_handler.is_edit_mode { if !event_handler.is_edit_mode {
let current_input = auth_state.get_current_input(); let current_input = auth_state.get_current_input();
let max_cursor_pos = if !current_input.is_empty() { let max_cursor_pos = if !current_input.is_empty() {
current_input.len() - 1 current_input.len() - 1
} else { } else {
0 0
}; };
auth_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos); auth_state.current_cursor_pos =
} event_handler.ideal_cursor_column.min(max_cursor_pos);
} }
// Only update command message if handle_event provided one
if !message.is_empty() {
event_handler.command_message = message;
} }
// Check exit condition *after* processing outcome
if should_exit { if should_exit {
// terminal.cleanup()?; // Optional: Drop handles this
return Ok(()); return Ok(());
} }
} }