Compare commits

...

19 Commits

Author SHA1 Message Date
filipriec
8c7a0a1ec0 we compiled centralized system for select 2025-04-10 12:13:04 +02:00
filipriec
a8eef8107b cursor fixed 2025-04-07 23:39:27 +02:00
filipriec
75cd942f39 fixed and now fully functional 2025-04-07 23:06:58 +02:00
filipriec
fc04af148d minor error fix 2025-04-07 22:55:07 +02:00
filipriec
d7d7fd614b fixing more errors, last to go 2025-04-07 22:46:00 +02:00
filipriec
10e9c3ead0 minor changes 2025-04-07 21:32:02 +02:00
filipriec
70678432c6 BREAKING CHANGES updating gRPC based on the enum now 2025-04-07 21:27:01 +02:00
filipriec
bb103fac6c minor changes 2025-04-07 17:36:59 +02:00
filipriec
0e1fc3f5fa cursor is disabled instead of hidden now 2025-04-07 17:23:42 +02:00
filipriec
7830ebdb3b cursor hidden if not active 2025-04-07 17:16:36 +02:00
filipriec
b061dd3395 BREAKING UPDATE COUNT REMOVED FROM THE MAIN LOOP 2025-04-07 16:27:40 +02:00
filipriec
a6f2fa8a88 button highlight now working perfectly well 2025-04-07 14:05:28 +02:00
filipriec
37f12ea6f0 working needs a small fix 2025-04-07 13:24:22 +02:00
filipriec
e29b576102 removed unused imports 2025-04-07 12:19:35 +02:00
filipriec
b1b3cf6136 changes for revert save in the readonly mode and not only the edit mode finished. 2025-04-07 12:18:03 +02:00
filipriec
173c4c98b8 feat: Prevent form navigation with unsaved changes 2025-04-07 12:03:17 +02:00
filipriec
5a3067c8e5 edit to read_only mode without save 2025-04-07 10:13:39 +02:00
filipriec
803c748738 only to the top and to the bottom in the canvas, cant jump around anymore 2025-04-06 23:16:15 +02:00
filipriec
5879b40e8c exit menu buttons upon success 2025-04-05 19:42:29 +02:00
31 changed files with 598 additions and 307 deletions

6
Cargo.lock generated
View File

@@ -421,7 +421,7 @@ dependencies = [
[[package]]
name = "client"
version = "0.3.0"
version = "0.3.3"
dependencies = [
"async-trait",
"common",
@@ -458,7 +458,7 @@ dependencies = [
[[package]]
name = "common"
version = "0.3.0"
version = "0.3.3"
dependencies = [
"prost",
"serde",
@@ -2589,7 +2589,7 @@ dependencies = [
[[package]]
name = "server"
version = "0.3.0"
version = "0.3.3"
dependencies = [
"bcrypt",
"chrono",

View File

@@ -5,7 +5,7 @@ resolver = "2"
[workspace.package]
# TODO: idk how to do the name, fix later
# name = "Multieko2"
version = "0.3.0"
version = "0.3.3"
edition = "2021"
license = "GPL-3.0-or-later"
authors = ["Filip Priečinský <filippriec@gmail.com>"]

View File

@@ -6,6 +6,7 @@ use crate::{
components::common::dialog,
state::state::AppState,
};
use crate::modes::handlers::mode_manager::AppMode;
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect, Margin},
style::{Style, Modifier, Color},
@@ -75,7 +76,12 @@ pub fn render_login(
.split(chunks[2]);
// Login Button
let login_active = !state.return_selected;
let login_button_index = 0;
let login_active = if app_state.ui.focus_outside_canvas {
app_state.general.selected_item == login_button_index
} else {
false
};
let mut login_style = Style::default().fg(theme.fg);
let mut login_border = Style::default().fg(theme.border);
if login_active {
@@ -97,7 +103,12 @@ pub fn render_login(
);
// Return Button
let return_active = state.return_selected;
let return_button_index = 1; // Assuming Return is the second general element
let return_active = if app_state.ui.focus_outside_canvas {
app_state.general.selected_item == return_button_index
} else {
false // Not active if focus is in canvas or other modes
};
let mut return_style = Style::default().fg(theme.fg);
let mut return_border = Style::default().fg(theme.border);
if return_active {

View File

@@ -27,13 +27,16 @@ pub fn render_canvas(
.split(area);
// Input container styling
let border_style = if form_state.has_unsaved_changes() {
Style::default().fg(theme.warning)
} else if is_edit_mode {
Style::default().fg(theme.accent)
} else {
Style::default().fg(theme.secondary)
};
let input_container = Block::default()
.borders(Borders::ALL)
.border_style(if is_edit_mode {
form_state.has_unsaved_changes().then(|| theme.warning).unwrap_or(theme.accent)
} else {
theme.secondary
})
.border_style(border_style)
.style(Style::default().bg(theme.bg));
// Input block dimensions

View File

@@ -3,5 +3,4 @@
pub mod common;
pub mod modes;
pub use common::*;
pub use modes::*;

View File

@@ -1,9 +1,8 @@
// src/functions/modes/edit/auth_e.rs
use crate::config::binds::config::Config;
use crate::services::grpc_client::GrpcClient;
use crate::state::canvas_state::CanvasState;
use crate::state::pages::form::FormState;
use crate::state::pages::{auth::AuthState, form::FormState};
use crate::tui::functions::common::form::{revert, save};
use crossterm::event::{KeyCode, KeyEvent};
use std::any::Any;
@@ -12,7 +11,6 @@ pub async fn execute_common_action<S: CanvasState + Any>(
action: &str,
state: &mut S,
grpc_client: &mut GrpcClient,
is_saved: &mut bool,
current_position: &mut u64,
total_count: u64,
) -> Result<String, Box<dyn std::error::Error>> {
@@ -26,14 +24,15 @@ pub async fn execute_common_action<S: CanvasState + Any>(
{
match action {
"save" => {
save(
let outcome = save(
form_state,
grpc_client,
is_saved,
current_position,
total_count,
)
.await
.await?;
let message = format!("Save successful: {:?}", outcome); // Simple message for now
Ok(message)
}
"revert" => {
revert(
@@ -62,10 +61,9 @@ pub async fn execute_edit_action<S: CanvasState>(
key: KeyEvent,
state: &mut S,
ideal_cursor_column: &mut usize,
_grpc_client: &mut GrpcClient,
_is_saved: &mut bool,
_current_position: &mut u64,
_total_count: u64,
grpc_client: &mut GrpcClient,
current_position: &mut u64,
total_count: u64,
) -> Result<String, Box<dyn std::error::Error>> {
match action {
"insert_char" => {
@@ -173,11 +171,7 @@ pub async fn execute_edit_action<S: CanvasState>(
let num_fields = state.fields().len();
if num_fields > 0 {
let current_field = state.current_field();
let new_field = if current_field == 0 {
num_fields - 1
} else {
current_field - 1
};
let new_field = current_field.saturating_sub(1);
state.set_current_field(new_field);
let current_input = state.get_current_input();
let max_pos = current_input.len();
@@ -191,7 +185,7 @@ pub async fn execute_edit_action<S: CanvasState>(
"move_down" => {
let num_fields = state.fields().len();
if num_fields > 0 {
let new_field = (state.current_field() + 1) % num_fields;
let new_field = (state.current_field() + 1).min(num_fields - 1);
state.set_current_field(new_field);
let current_input = state.get_current_input();
let max_pos = current_input.len();

View File

@@ -1,10 +1,11 @@
// src/functions/modes/edit/form_e.rs
use crate::config::binds::config::Config;
use crate::services::grpc_client::GrpcClient;
use crate::state::canvas_state::CanvasState;
use crate::state::pages::form::FormState;
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 std::any::Any;
@@ -12,48 +13,61 @@ pub async fn execute_common_action<S: CanvasState + Any>(
action: &str,
state: &mut S,
grpc_client: &mut GrpcClient,
is_saved: &mut bool,
current_position: &mut u64,
total_count: u64,
) -> Result<String, Box<dyn std::error::Error>> {
) -> Result<EventOutcome, Box<dyn std::error::Error>> {
match action {
"save" | "revert" => {
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) =
(state as &mut dyn Any).downcast_mut::<FormState>()
{
match action {
"save" => {
save(
let save_result = save(
form_state,
grpc_client,
is_saved,
current_position,
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(
let revert_result = revert(
form_state,
grpc_client,
current_position,
total_count,
)
.await
).await;
match revert_result {
Ok(message) => Ok(EventOutcome::Ok(message)),
Err(e) => Err(e),
}
}
_ => unreachable!(),
}
} else {
Ok(format!(
Ok(EventOutcome::Ok(format!(
"Action '{}' not implemented for this state type.",
action
))
)))
}
}
_ => Ok(format!("Common action '{}' not handled here.", action)),
_ => Ok(EventOutcome::Ok(format!("Common action '{}' not handled here.", action))),
}
}
@@ -62,10 +76,9 @@ pub async fn execute_edit_action<S: CanvasState>(
key: KeyEvent,
state: &mut S,
ideal_cursor_column: &mut usize,
_grpc_client: &mut GrpcClient,
_is_saved: &mut bool,
_current_position: &mut u64,
_total_count: u64,
grpc_client: &mut GrpcClient,
current_position: &mut u64,
total_count: u64,
) -> Result<String, Box<dyn std::error::Error>> {
match action {
"insert_char" => {
@@ -173,11 +186,7 @@ pub async fn execute_edit_action<S: CanvasState>(
let num_fields = state.fields().len();
if num_fields > 0 {
let current_field = state.current_field();
let new_field = if current_field == 0 {
num_fields - 1
} else {
current_field - 1
};
let new_field = current_field.saturating_sub(1);
state.set_current_field(new_field);
let current_input = state.get_current_input();
let max_pos = current_input.len();
@@ -191,7 +200,7 @@ pub async fn execute_edit_action<S: CanvasState>(
"move_down" => {
let num_fields = state.fields().len();
if num_fields > 0 {
let new_field = (state.current_field() + 1) % num_fields;
let new_field = (state.current_field() + 1).min(num_fields - 1);
state.set_current_field(new_field);
let current_input = state.get_current_input();
let max_pos = current_input.len();

View File

@@ -2,6 +2,7 @@
use crate::config::binds::key_sequences::KeySequenceTracker;
use crate::state::canvas_state::CanvasState;
use crate::state::state::AppState;
use std::error::Error;
#[derive(PartialEq)]
@@ -13,6 +14,7 @@ enum CharType {
pub async fn execute_action<S: CanvasState>(
action: &str,
app_state: &mut AppState,
state: &mut S,
ideal_cursor_column: &mut usize,
key_sequence_tracker: &mut KeySequenceTracker,
@@ -33,11 +35,7 @@ pub async fn execute_action<S: CanvasState>(
return Ok("No fields to navigate.".to_string());
}
let current_field = state.current_field();
let new_field = if current_field == 0 {
num_fields - 1
} else {
current_field - 1
};
let new_field = current_field.saturating_sub(1);
state.set_current_field(new_field);
let current_input = state.get_current_input();
let max_cursor_pos = if current_input.is_empty() {
@@ -57,17 +55,28 @@ pub async fn execute_action<S: CanvasState>(
return Ok("No fields to navigate.".to_string());
}
let current_field = state.current_field();
let new_field = (current_field + 1) % num_fields;
state.set_current_field(new_field);
let current_input = state.get_current_input();
let max_cursor_pos = if current_input.is_empty() {
0
let last_field_index = num_fields - 1;
if current_field == last_field_index {
// Already on the last field, move focus outside
app_state.ui.focus_outside_canvas = true;
app_state.general.selected_item = 0; // Focus first general item (e.g., Login button)
key_sequence_tracker.reset();
Ok("Focus moved below canvas".to_string())
} else {
current_input.len().saturating_sub(1)
};
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
state.set_current_cursor_pos(new_pos);
Ok("move down from functions/modes/read_only/auth_ro.rs".to_string())
// Move to the next field within the canvas
let new_field = (current_field + 1).min(last_field_index);
state.set_current_field(new_field);
let current_input = state.get_current_input();
let max_cursor_pos = if current_input.is_empty() {
0
} else {
current_input.len().saturating_sub(1)
};
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
state.set_current_cursor_pos(new_pos);
Ok("".to_string()) // Clear previous debug message
}
}
"move_first_line" => {
key_sequence_tracker.reset();

View File

@@ -33,11 +33,7 @@ pub async fn execute_action<S: CanvasState>(
return Ok("No fields to navigate.".to_string());
}
let current_field = state.current_field();
let new_field = if current_field == 0 {
num_fields - 1
} else {
current_field - 1
};
let new_field = current_field.saturating_sub(1);
state.set_current_field(new_field);
let current_input = state.get_current_input();
let max_cursor_pos = if current_input.is_empty() {
@@ -56,7 +52,7 @@ pub async fn execute_action<S: CanvasState>(
return Ok("No fields to navigate.".to_string());
}
let current_field = state.current_field();
let new_field = (current_field + 1) % num_fields;
let new_field = (current_field + 1).min(num_fields - 1);
state.set_current_field(new_field);
let current_input = state.get_current_input();
let max_cursor_pos = if current_input.is_empty() {

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

@@ -3,8 +3,8 @@
use crate::config::binds::config::Config;
use crate::services::grpc_client::GrpcClient;
use crate::state::pages::{auth::AuthState, form::FormState};
use crate::state::state::AppState;
use crate::functions::modes::edit::{auth_e, form_e};
use crate::modes::handlers::event::EventOutcome;
use crossterm::event::{KeyCode, KeyEvent};
pub async fn handle_edit_event(
@@ -15,7 +15,6 @@ pub async fn handle_edit_event(
auth_state: &mut AuthState,
ideal_cursor_column: &mut usize,
command_message: &mut String,
is_saved: &mut bool,
current_position: &mut u64,
total_count: u64,
grpc_client: &mut GrpcClient,
@@ -38,12 +37,11 @@ pub async fn handle_edit_event(
key.modifiers
) {
if matches!(action, "save" | "revert") {
return if is_auth_context {
let message = if is_auth_context {
auth_e::execute_common_action(
action,
auth_state, // Concrete AuthState
grpc_client,
is_saved,
current_position,
total_count
).await
@@ -52,11 +50,19 @@ pub async fn handle_edit_event(
action,
form_state, // Concrete FormState
grpc_client,
is_saved,
current_position,
total_count
).await
};
.map(|outcome| match outcome {
EventOutcome::Ok(msg) => msg,
EventOutcome::Exit(msg) => format!("Exit requested: {}", msg), // Or handle differently
EventOutcome::DataSaved(save_outcome, msg) => format!("Data saved ({:?}): {}", save_outcome, msg),
EventOutcome::ButtonSelected { context, index } => {
"Unexpected action in edit mode".to_string()
}
})
}?;
return Ok(message);
}
}
@@ -69,7 +75,6 @@ pub async fn handle_edit_event(
auth_state, // Full access to AuthState fields
ideal_cursor_column,
grpc_client,
is_saved,
current_position,
total_count
).await
@@ -80,7 +85,6 @@ pub async fn handle_edit_event(
form_state, // Full access to FormState fields
ideal_cursor_column,
grpc_client,
is_saved,
current_position,
total_count
).await
@@ -96,7 +100,6 @@ pub async fn handle_edit_event(
auth_state,
ideal_cursor_column,
grpc_client,
is_saved,
current_position,
total_count
).await
@@ -107,7 +110,6 @@ pub async fn handle_edit_event(
form_state,
ideal_cursor_column,
grpc_client,
is_saved,
current_position,
total_count
).await

View File

@@ -6,11 +6,12 @@ use crate::services::grpc_client::GrpcClient;
use crate::state::canvas_state::CanvasState;
use crate::state::pages::auth::AuthState;
use crate::state::pages::form::FormState;
use crate::state::state::AppState;
use crate::functions::modes::read_only::{auth_ro, form_ro};
use crossterm::event::KeyEvent;
pub async fn handle_read_only_event(
app_state: &crate::state::state::AppState,
app_state: &mut AppState,
key: KeyEvent,
config: &Config,
form_state: &mut FormState,
@@ -90,6 +91,7 @@ pub async fn handle_read_only_event(
} else if app_state.ui.show_login {
auth_ro::execute_action(
action,
app_state,
auth_state,
ideal_cursor_column,
key_sequence_tracker,
@@ -136,6 +138,7 @@ pub async fn handle_read_only_event(
} else if app_state.ui.show_login {
auth_ro::execute_action(
action,
app_state,
auth_state,
ideal_cursor_column,
key_sequence_tracker,
@@ -181,6 +184,7 @@ pub async fn handle_read_only_event(
} else if app_state.ui.show_login {
auth_ro::execute_action(
action,
app_state,
auth_state,
ideal_cursor_column,
key_sequence_tracker,

View File

@@ -1,16 +1,22 @@
// src/modes/handlers/command_mode.rs
// src/modes/common/command_mode.rs
use crossterm::event::{KeyEvent, KeyCode, KeyModifiers};
use crate::config::binds::config::Config;
use crate::services::grpc_client::GrpcClient;
use crate::state::pages::form::FormState;
use crate::state::{state::AppState, pages::auth::AuthState};
use crate::modes::common::commands::CommandHandler;
use crate::tui::terminal::core::TerminalCore;
use crate::tui::functions::common::form::{save, revert};
use crate::modes::handlers::event::EventOutcome;
use crate::tui::functions::common::form::SaveOutcome;
use std::error::Error;
pub async fn handle_command_event(
key: KeyEvent,
config: &Config,
app_state: &AppState,
auth_state: &AuthState,
form_state: &mut FormState,
command_input: &mut String,
command_message: &mut String,
@@ -19,15 +25,12 @@ pub async fn handle_command_event(
terminal: &mut TerminalCore,
current_position: &mut u64,
total_count: u64,
) -> Result<(bool, String, bool), Box<dyn std::error::Error>> {
// Return value: (should_exit, message, should_exit_command_mode)
) -> Result<EventOutcome, Box<dyn Error>> {
// Exit command mode (via configurable keybinding)
if config.is_exit_command_mode(key.code, key.modifiers) {
command_input.clear();
*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)
@@ -35,6 +38,8 @@ pub async fn handle_command_event(
return process_command(
config,
form_state,
app_state,
auth_state,
command_input,
command_message,
grpc_client,
@@ -48,7 +53,7 @@ pub async fn handle_command_event(
// Backspace (via configurable keybinding, defaults to Backspace)
if config.is_command_backspace(key.code, key.modifiers) {
command_input.pop();
return Ok((false, "".to_string(), false));
return Ok(EventOutcome::Ok("".to_string()));
}
// Regular character input - accept any character in command mode
@@ -56,17 +61,19 @@ pub async fn handle_command_event(
// Accept regular or shifted characters (e.g., 'a' or 'A')
if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT {
command_input.push(c);
return Ok((false, "".to_string(), false));
return Ok(EventOutcome::Ok("".to_string()));
}
}
// Ignore all other keys
Ok((false, "".to_string(), false))
Ok(EventOutcome::Ok("".to_string()))
}
async fn process_command(
config: &Config,
form_state: &mut FormState,
app_state: &AppState,
auth_state: &AuthState,
command_input: &mut String,
command_message: &mut String,
grpc_client: &mut GrpcClient,
@@ -74,12 +81,12 @@ async fn process_command(
terminal: &mut TerminalCore,
current_position: &mut u64,
total_count: u64,
) -> Result<(bool, String, bool), Box<dyn std::error::Error>> {
) -> Result<EventOutcome, Box<dyn Error>> {
// Clone the trimmed command to avoid borrow issues
let command = command_input.trim().to_string();
if command.is_empty() {
*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)
@@ -89,21 +96,35 @@ async fn process_command(
match action {
"force_quit" | "save_and_quit" | "quit" => {
let (should_exit, message) = command_handler
.handle_command(action, terminal)
.handle_command(
action,
terminal,
app_state,
form_state,
auth_state,
)
.await?;
command_input.clear();
Ok((should_exit, message, true))
if should_exit {
Ok(EventOutcome::Exit(message))
} else {
Ok(EventOutcome::Ok(message))
}
},
"save" => {
let message = save(
let outcome = save(
form_state,
grpc_client,
&mut command_handler.is_saved,
current_position,
total_count,
).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();
return Ok((false, message, true));
Ok(EventOutcome::DataSaved(outcome, message))
},
"revert" => {
let message = revert(
@@ -113,17 +134,12 @@ async fn process_command(
total_count,
).await?;
command_input.clear();
return Ok((false, message, true));
},
"unknown" => {
let message = format!("Unknown command: {}", command);
command_input.clear();
return Ok((false, message, true));
Ok(EventOutcome::Ok(message))
},
_ => {
let message = format!("Unhandled action: {}", action);
command_input.clear();
return Ok((false, message, true));
Ok(EventOutcome::Ok(message))
}
}
}

View File

@@ -1,22 +1,26 @@
// src/tui/controls/commands.rs
// src/modes/common/commands.rs
use crate::tui::terminal::core::TerminalCore;
use crate::state::state::AppState;
use crate::state::pages::{form::FormState, auth::AuthState};
use crate::state::canvas_state::CanvasState;
pub struct CommandHandler {
pub is_saved: bool,
}
pub struct CommandHandler;
impl CommandHandler {
pub fn new() -> Self {
Self { is_saved: false }
Self
}
pub async fn handle_command(
&mut self,
action: &str,
terminal: &mut TerminalCore,
app_state: &AppState,
form_state: &FormState,
auth_state: &AuthState,
) -> Result<(bool, String), Box<dyn std::error::Error>> {
match action {
"quit" => self.handle_quit(terminal).await,
"quit" => self.handle_quit(terminal, app_state, form_state, auth_state).await,
"force_quit" => self.handle_force_quit(terminal).await,
"save_and_quit" => self.handle_save_quit(terminal).await,
_ => Ok((false, format!("Unknown command: {}", action))),
@@ -26,8 +30,18 @@ impl CommandHandler {
async fn handle_quit(
&self,
terminal: &mut TerminalCore,
app_state: &AppState,
form_state: &FormState,
auth_state: &AuthState,
) -> Result<(bool, String), Box<dyn std::error::Error>> {
if self.is_saved {
// Use actual unsaved changes state instead of is_saved flag
let has_unsaved = if app_state.ui.show_login {
auth_state.has_unsaved_changes()
} else {
form_state.has_unsaved_changes
};
if !has_unsaved {
terminal.cleanup()?;
Ok((true, "Exiting.".into()))
} else {
@@ -47,7 +61,6 @@ impl CommandHandler {
&mut self,
terminal: &mut TerminalCore,
) -> Result<(bool, String), Box<dyn std::error::Error>> {
self.is_saved = true;
terminal.cleanup()?;
Ok((true, "State saved. Exiting.".into()))
}

View File

@@ -4,65 +4,88 @@ use crossterm::event::KeyEvent;
use crate::config::binds::config::Config;
use crate::state::state::AppState;
use crate::state::pages::form::FormState;
use crate::tui::functions::{intro, admin};
use crate::state::pages::auth::AuthState;
use crate::state::canvas_state::CanvasState;
use crate::ui::handlers::context::UiContext;
use crate::modes::handlers::event::EventOutcome;
pub async fn handle_navigation_event(
key: KeyEvent,
config: &Config,
form_state: &mut FormState,
app_state: &mut AppState,
auth_state: &mut AuthState,
command_mode: &mut bool,
command_input: &mut String,
command_message: &mut String,
) -> Result<(bool, String), Box<dyn std::error::Error>> {
) -> Result<EventOutcome, Box<dyn std::error::Error>> {
if let Some(action) = config.get_general_action(key.code, key.modifiers) {
match action {
"move_up" => {
move_up(app_state);
return Ok((false, String::new()));
move_up(app_state, auth_state);
return Ok(EventOutcome::Ok(String::new()));
}
"move_down" => {
move_down(app_state);
return Ok((false, String::new()));
return Ok(EventOutcome::Ok(String::new()));
}
"next_option" => {
next_option(app_state); // Intro has 2 options
return Ok((false, String::new()));
next_option(app_state);
return Ok(EventOutcome::Ok(String::new()));
}
"previous_option" => {
previous_option(app_state);
return Ok((false, String::new()));
}
"select" => {
select(app_state);
return Ok((false, "Selected".to_string()));
return Ok(EventOutcome::Ok(String::new()));
}
"toggle_sidebar" => {
toggle_sidebar(app_state);
return Ok((false, format!("Sidebar {}",
return Ok(EventOutcome::Ok(format!("Sidebar {}",
if app_state.ui.show_sidebar { "shown" } else { "hidden" }
)));
}
"next_field" => {
next_field(form_state);
return Ok((false, String::new()));
return Ok(EventOutcome::Ok(String::new()));
}
"prev_field" => {
prev_field(form_state);
return Ok((false, String::new()));
return Ok(EventOutcome::Ok(String::new()));
}
"enter_command_mode" => {
handle_enter_command_mode(command_mode, command_input, command_message);
return Ok((false, String::new()));
return Ok(EventOutcome::Ok(String::new()));
}
"select" => {
let (context, index) = if app_state.ui.show_intro {
(UiContext::Intro, app_state.ui.intro_state.selected_option)
} else if app_state.ui.show_login && app_state.ui.focus_outside_canvas {
(UiContext::Login, app_state.general.selected_item)
} else if app_state.ui.show_admin {
(UiContext::Admin, app_state.general.selected_item)
} else if app_state.ui.dialog.dialog_show {
(UiContext::Dialog, app_state.ui.dialog.dialog_active_button_index)
} else {
// Handle cases where select is pressed but no button context applies
return Ok(EventOutcome::Ok("Select (No Action)".to_string()));
};
return Ok(EventOutcome::ButtonSelected { context, index });
}
_ => {}
}
}
Ok((false, String::new()))
Ok(EventOutcome::Ok(String::new()))
}
pub fn move_up(app_state: &mut AppState) {
if app_state.ui.show_intro {
pub fn move_up(app_state: &mut AppState, auth_state: &mut AuthState) {
if app_state.ui.focus_outside_canvas && app_state.ui.show_login {
if app_state.general.selected_item == 0 {
app_state.ui.focus_outside_canvas = false;
let last_field_index = auth_state.fields().len().saturating_sub(1);
auth_state.set_current_field(last_field_index);
} else {
app_state.general.selected_item = app_state.general.selected_item.saturating_sub(1);
}
} else if app_state.ui.show_intro {
app_state.ui.intro_state.previous_option();
} else if app_state.ui.show_admin {
// Assuming profile_tree.profiles is the list we're navigating
@@ -81,7 +104,12 @@ pub fn move_up(app_state: &mut AppState) {
}
pub fn move_down(app_state: &mut AppState) {
if app_state.ui.show_intro {
if app_state.ui.focus_outside_canvas && app_state.ui.show_login {
let num_general_elements = 2;
if app_state.general.selected_item < num_general_elements - 1 {
app_state.general.selected_item += 1;
}
} else if app_state.ui.show_intro {
app_state.ui.intro_state.next_option();
} else if app_state.ui.show_admin {
// Assuming profile_tree.profiles is the list we're navigating
@@ -117,14 +145,6 @@ pub fn previous_option(app_state: &mut AppState) {
}
}
pub fn select(app_state: &mut AppState) {
if app_state.ui.show_intro {
intro::handle_intro_selection(app_state);
} else if app_state.ui.show_admin {
admin::handle_admin_selection(app_state);
}
}
pub fn toggle_sidebar(app_state: &mut AppState) {
app_state.ui.show_sidebar = !app_state.ui.show_sidebar;
}

View File

@@ -1,9 +1,7 @@
// src/modes/handlers/event.rs
use crossterm::event::Event;
use crossterm::cursor::SetCursorStyle;
use crate::tui::terminal::{
core::TerminalCore,
};
use crate::tui::terminal::core::TerminalCore;
use crate::services::grpc_client::GrpcClient;
use crate::services::auth::AuthClient;
use crate::modes::common::commands::CommandHandler;
@@ -12,13 +10,25 @@ use crate::state::pages::form::FormState;
use crate::state::pages::auth::AuthState;
use crate::state::canvas_state::CanvasState;
use crate::ui::handlers::rat_state::UiStateHandler;
use crate::ui::handlers::context::UiContext;
use crate::tui::functions::{intro, admin};
use crate::tui::functions::common::login;
use crate::modes::{
common::{command_mode},
common::command_mode,
canvas::{edit, read_only, common_mode},
general::navigation,
};
use crate::config::binds::key_sequences::KeySequenceTracker;
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),
Exit(String),
DataSaved(SaveOutcome, String),
ButtonSelected { context: UiContext, index: usize },
}
pub struct EventHandler {
pub command_mode: bool,
@@ -28,7 +38,6 @@ pub struct EventHandler {
pub edit_mode_cooldown: bool,
pub ideal_cursor_column: usize,
pub key_sequence_tracker: KeySequenceTracker,
// pub auth_state: AuthState, // Removed
pub auth_client: AuthClient,
}
@@ -42,7 +51,6 @@ impl EventHandler {
edit_mode_cooldown: false,
ideal_cursor_column: 0,
key_sequence_tracker: KeySequenceTracker::new(800),
// auth_state: AuthState::new(), // Removed
auth_client: AuthClient::new().await?,
})
}
@@ -55,12 +63,11 @@ impl EventHandler {
grpc_client: &mut GrpcClient,
command_handler: &mut CommandHandler,
form_state: &mut FormState,
auth_state: &mut AuthState, // Added
auth_state: &mut AuthState,
app_state: &mut crate::state::state::AppState,
total_count: u64,
current_position: &mut u64,
) -> Result<(bool, String), Box<dyn std::error::Error>> {
// Determine current mode based on app state and event handler state
) -> Result<EventOutcome, Box<dyn std::error::Error>> {
let current_mode = ModeManager::derive_mode(app_state, self);
app_state.update_mode(current_mode);
@@ -68,36 +75,71 @@ impl EventHandler {
let key_code = key.code;
let modifiers = key.modifiers;
// Handle common actions across all modes
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" }
)));
);
return Ok(EventOutcome::Ok(message));
}
// Mode-specific handling
match current_mode {
AppMode::General => {
return navigation::handle_navigation_event(
let nav_outcome = navigation::handle_navigation_event(
key,
config,
form_state,
app_state,
auth_state,
&mut self.command_mode,
&mut self.command_input,
&mut self.command_message,
).await;
match nav_outcome {
Ok(EventOutcome::ButtonSelected { context, index }) => {
let mut message = String::from("Selected"); // Default message
match context {
UiContext::Intro => {
intro::handle_intro_selection(app_state, index); // Pass index
message = format!("Intro Option {} selected", index);
}
UiContext::Login => {
message = match index {
0 => login::save(auth_state, &mut self.auth_client, app_state).await?,
1 => {
let msg = login::revert(auth_state, app_state).await;
// Optional: Add navigation logic here if revert should change screen
// app_state.ui.show_login = false;
// app_state.ui.show_intro = true;
msg
}
_ => "Invalid Login Option".to_string(),
};
}
UiContext::Admin => {
// Assuming handle_admin_selection uses app_state.general.selected_item
admin::handle_admin_selection(app_state);
message = format!("Admin Option {} selected", index);
}
UiContext::Dialog => {
// Add specific dialog handling logic here
message = format!("Dialog Button {} selected", index);
app_state.hide_dialog(); // Example action
}
}
return Ok(EventOutcome::Ok(message)); // Return Ok with message
}
other => return other, // Pass through Ok, Err, DataSaved directly
}
},
AppMode::ReadOnly => {
// Check for mode transitions first
if config.is_enter_edit_mode_before(key_code, modifiers) &&
ModeManager::can_enter_edit_mode(current_mode) {
self.is_edit_mode = true;
self.edit_mode_cooldown = true;
self.command_message = "Edit mode".to_string();
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) &&
@@ -126,20 +168,18 @@ impl EventHandler {
self.edit_mode_cooldown = true;
self.command_message = "Edit mode (after cursor)".to_string();
terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?;
return Ok((false, self.command_message.clone()));
return Ok(EventOutcome::Ok(self.command_message.clone()));
}
// Check for entering command mode
if let Some(action) = config.get_read_only_action_for_key(key_code, modifiers) {
if action == "enter_command_mode" && ModeManager::can_enter_command_mode(current_mode) {
self.command_mode = true;
self.command_input.clear();
self.command_message.clear();
return Ok((false, String::new()));
return Ok(EventOutcome::Ok(String::new()));
}
}
// Check for core application actions (save, quit, etc.)
if let Some(action) = config.get_action_for_key_in_mode(
&config.keybindings.common,
key_code,
@@ -150,7 +190,7 @@ impl EventHandler {
return common_mode::handle_core_action(
action,
form_state,
auth_state, // Changed
auth_state,
grpc_client,
&mut self.auth_client,
terminal,
@@ -163,13 +203,12 @@ impl EventHandler {
}
}
// Let read_only mode handle its own actions
return read_only::handle_read_only_event(
&app_state,
let (_should_exit, message) = read_only::handle_read_only_event(
app_state,
key,
config,
form_state,
auth_state, // Changed
auth_state,
&mut self.key_sequence_tracker,
current_position,
total_count,
@@ -177,25 +216,27 @@ impl EventHandler {
&mut self.command_message,
&mut self.edit_mode_cooldown,
&mut self.ideal_cursor_column,
).await;
).await?;
return Ok(EventOutcome::Ok(message));
},
AppMode::Edit => {
// Check for exiting edit mode
if config.is_exit_edit_mode(key_code, modifiers) {
self.is_edit_mode = false;
self.edit_mode_cooldown = true;
let has_changes = if app_state.ui.show_login {
auth_state.has_unsaved_changes()
} else {
form_state.has_unsaved_changes()
};
if has_changes {
self.command_message = "Unsaved changes! Use :w to save or :q! to discard".to_string();
return Ok((false, self.command_message.clone()));
}
self.is_edit_mode = false;
self.edit_mode_cooldown = true;
self.command_message = "Read-only mode".to_string();
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)?;
let current_input = if app_state.ui.show_login {
@@ -219,10 +260,9 @@ impl EventHandler {
self.ideal_cursor_column = form_state.current_cursor_pos();
}
}
return Ok((false, self.command_message.clone()));
return Ok(EventOutcome::Ok(self.command_message.clone()));
}
// Check for core application actions (save, quit, etc.)
if let Some(action) = config.get_action_for_key_in_mode(
&config.keybindings.common,
key_code,
@@ -233,7 +273,7 @@ impl EventHandler {
return common_mode::handle_core_action(
action,
form_state,
auth_state, // Changed
auth_state,
grpc_client,
&mut self.auth_client,
terminal,
@@ -246,29 +286,29 @@ impl EventHandler {
}
}
// Let edit mode handle its own actions
let result = edit::handle_edit_event(
let message = edit::handle_edit_event(
app_state.ui.show_login,
key,
config,
form_state,
auth_state, // Changed
auth_state,
&mut self.ideal_cursor_column,
&mut self.command_message,
&mut app_state.ui.is_saved,
current_position,
total_count,
grpc_client,
).await?;
self.key_sequence_tracker.reset();
return Ok((false, result));
return Ok(EventOutcome::Ok(message));
},
AppMode::Command => {
let (should_exit, message, exit_command_mode) = command_mode::handle_command_event(
let outcome = command_mode::handle_command_event(
key,
config,
app_state,
auth_state,
form_state,
&mut self.command_input,
&mut self.command_message,
@@ -279,18 +319,17 @@ impl EventHandler {
total_count,
).await?;
if exit_command_mode {
self.command_mode = false;
if let EventOutcome::Ok(msg) = &outcome {
if msg == "Exited command mode" {
self.command_mode = false;
}
}
return Ok((should_exit, message));
return Ok(outcome);
}
}
}
// Non-key events or if no specific handler was matched
self.edit_mode_cooldown = false;
Ok((false, self.command_message.clone()))
Ok(EventOutcome::Ok(self.command_message.clone()))
}
}

View File

@@ -19,6 +19,10 @@ impl ModeManager {
return AppMode::Command;
}
if app_state.ui.focus_outside_canvas {
return AppMode::General;
}
if app_state.ui.show_login {
if event_handler.is_edit_mode {
AppMode::Edit

View File

@@ -2,6 +2,7 @@
use crate::services::grpc_client::GrpcClient;
use crate::state::pages::form::FormState;
use crate::tui::functions::common::form::SaveOutcome;
use crate::state::state::AppState;
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

@@ -1,6 +1,5 @@
// src/state/canvas_state.rs
use crate::state::pages::form::FormState;
pub trait CanvasState {
fn current_field(&self) -> usize;

View File

@@ -15,12 +15,12 @@ pub struct DialogState {
pub struct UiState {
pub show_sidebar: bool,
pub is_saved: bool,
pub show_intro: bool,
pub show_admin: bool,
pub show_form: bool,
pub show_login: bool,
pub intro_state: IntroState,
pub focus_outside_canvas: bool,
pub dialog: DialogState, // Add dialog state here
}
@@ -133,12 +133,12 @@ impl Default for UiState {
fn default() -> Self {
Self {
show_sidebar: true,
is_saved: false,
show_intro: true,
show_admin: false,
show_form: false,
show_login: false,
intro_state: IntroState::new(),
focus_outside_canvas: false,
dialog: DialogState::default(),
}
}

View File

@@ -9,4 +9,3 @@ pub mod common;
pub use admin::*;
pub use intro::*;
pub use form::*;
pub use common::*;

View File

@@ -3,5 +3,3 @@
pub mod form;
pub mod login;
pub use form::*;
pub use login::*;

View File

@@ -4,17 +4,26 @@ use crate::services::grpc_client::GrpcClient;
use crate::state::pages::form::FormState;
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
pub async fn save(
form_state: &mut FormState,
grpc_client: &mut GrpcClient,
is_saved: &mut bool,
current_position: &mut 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 message = if is_new {
let outcome = if is_new {
let post_request = PostAdresarRequest {
firma: form_state.values[0].clone(),
kz: form_state.values[1].clone(),
@@ -33,10 +42,9 @@ pub async fn save(
fax: form_state.values[14].clone(),
};
let response = grpc_client.post_adresar(post_request).await?;
let new_total = grpc_client.get_adresar_count().await?;
*current_position = new_total;
form_state.id = response.into_inner().id;
"New entry created".to_string()
let new_id = response.into_inner().id;
form_state.id = new_id;
SaveOutcome::CreatedNew(new_id) // <-- Return CreatedNew with ID
} else {
let put_request = PutAdresarRequest {
id: form_state.id,
@@ -57,12 +65,11 @@ pub async fn save(
fax: form_state.values[14].clone(),
};
let _ = grpc_client.put_adresar(put_request).await?;
"Entry updated".to_string()
SaveOutcome::UpdatedExisting
};
*is_saved = true;
form_state.has_unsaved_changes = false;
Ok(message)
Ok(outcome)
}
/// Discard changes since last save

View File

@@ -45,16 +45,13 @@ pub async fn save(
);
// Use the helper method to configure and show the dialog
// TODO Implement logic for pressing menu or exit buttons, not imeplementing it now,
// need to do other more important stuff now"
app_state.show_dialog(
"Login Success",
&success_message,
vec!["OK".to_string()], // Pass buttons here
vec!["Menu".to_string(), "Exit".to_string()],
);
// REMOVE these lines:
// app_state.ui.dialog.dialog_title = "Login Success".to_string();
// app_state.ui.dialog.dialog_message = success_message;
// app_state.ui.dialog.dialog_show = true;
// app_state.ui.dialog.dialog_button_active = true;
Ok("Login successful, details shown in dialog.".to_string())
}
@@ -83,7 +80,7 @@ pub async fn save(
/// Reverts the login form fields to empty and returns to the previous screen (Intro).
pub async fn revert(
auth_state: &mut AuthState,
_app_state: &mut AppState, // Prefix unused variable
app_state: &mut AppState,
) -> String {
// Clear the input fields
auth_state.username.clear();

View File

@@ -11,6 +11,15 @@ pub async fn handle_action(
total_count: u64,
ideal_cursor_column: &mut usize,
) -> Result<String, Box<dyn std::error::Error>> {
// TODO store unsaved changes without deleting form state values
// First check for unsaved changes in both cases
if form_state.has_unsaved_changes() {
return Ok(
"Unsaved changes. Save (Ctrl+S) or Revert (Ctrl+R) before navigating."
.to_string(),
);
}
match action {
"previous_entry" => {
let new_position = current_position.saturating_sub(1);

View File

@@ -1,7 +1,7 @@
use crate::state::state::AppState;
pub fn handle_intro_selection(app_state: &mut AppState) {
match app_state.ui.intro_state.selected_option {
pub fn handle_intro_selection(app_state: &mut AppState, index: usize) { // Add index parameter
match index { // Use index directly
0 => { // Continue
app_state.ui.show_form = true;
app_state.ui.show_admin = false;
@@ -21,3 +21,4 @@ pub fn handle_intro_selection(app_state: &mut AppState) {
}
app_state.ui.show_intro = false;
}

View File

@@ -1,6 +1,5 @@
// src/tui/functions/login.rs
use crate::state::pages::auth::AuthState;
use crate::state::canvas_state::CanvasState;
pub async fn handle_action(
action: &str,

View File

@@ -3,7 +3,7 @@
use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
cursor::{SetCursorStyle, EnableBlinking, Show, MoveTo},
cursor::{SetCursorStyle, EnableBlinking, Show, Hide, MoveTo},
};
use ratatui::{backend::CrosstermBackend, Terminal};
use std::io::{self, stdout, Write};
@@ -64,6 +64,22 @@ impl TerminalCore {
)?;
Ok(())
}
pub fn show_cursor(&mut self) -> Result<(), Box<dyn std::error::Error>> {
execute!(
self.terminal.backend_mut(),
Show
)?;
Ok(())
}
pub fn hide_cursor(&mut self) -> Result<(), Box<dyn std::error::Error>> {
execute!(
self.terminal.backend_mut(),
Hide
)?;
Ok(())
}
}
impl Drop for TerminalCore {

View File

@@ -3,6 +3,8 @@
pub mod ui;
pub mod render;
pub mod rat_state;
pub mod context;
pub use ui::run_ui;
pub use rat_state::*;
pub use context::*;

View File

@@ -0,0 +1,8 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UiContext {
Intro,
Login,
Admin,
Dialog,
}

View File

@@ -1,25 +1,25 @@
// src/ui/handlers/ui.rs
use crate::tui::terminal::TerminalCore;
use crate::services::grpc_client::GrpcClient;
use crate::services::auth::AuthClient;
use crate::services::ui_service::UiService;
use crate::tui::terminal::EventReader;
use crate::modes::common::commands::CommandHandler;
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::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::ui_service::UiService;
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::tui::functions::common::form::SaveOutcome; // Import SaveOutcome
use crate::tui::terminal::{EventReader, TerminalCore};
use crate::ui::handlers::render::render_ui;
use crossterm::cursor::SetCursorStyle;
pub async fn run_ui() -> Result<(), Box<dyn std::error::Error>> {
let config = Config::load()?;
let mut terminal = TerminalCore::new()?;
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 theme = Theme::from_str(&config.colors.theme);
let mut auth_state = AuthState::default(); // The single source of truth for AuthState
@@ -28,7 +28,9 @@ pub async fn run_ui() -> Result<(), Box<dyn std::error::Error>> {
let mut app_state = AppState::new()?;
// 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
let mut form_state = FormState::new(column_names);
@@ -38,12 +40,11 @@ pub async fn run_ui() -> Result<(), Box<dyn std::error::Error>> {
let event_reader = EventReader::new();
// 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();
loop {
UiService::update_adresar_count(&mut grpc_client, &mut app_state).await?;
// Determine edit mode based on EventHandler state
let is_edit_mode = event_handler.is_edit_mode;
@@ -64,94 +65,197 @@ pub async fn run_ui() -> Result<(), Box<dyn std::error::Error>> {
);
})?;
let total_count = app_state.total_count;
// --- Cursor Visibility Logic ---
let current_mode = ModeManager::derive_mode(&app_state, &event_handler);
match current_mode {
AppMode::Edit => {
terminal.show_cursor()?;
}
AppMode::ReadOnly => {
if !app_state.ui.focus_outside_canvas {
terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?;
} else {
terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?;
}
terminal.show_cursor()?;
}
AppMode::General => {
if app_state.ui.focus_outside_canvas {
terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?;
terminal.show_cursor()?;
} else {
terminal.hide_cursor()?;
}
}
AppMode::Command => {
terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?;
terminal.show_cursor()?;
}
}
// --- End Cursor Visibility Logic ---
let total_count = app_state.total_count; // Keep track for save logic
let mut current_position = app_state.current_position;
let position_before_event = current_position;
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;
// --- 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
}
EventOutcome::ButtonSelected { context, index } => {
event_handler.command_message = "Internal error: Unexpected button state".to_string();
}
},
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)
if app_state.ui.show_form { // Added check
if !event_handler.is_edit_mode {
if app_state.ui.show_form {
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 // Limit to last character in readonly mode
current_input.len() - 1 // Limit to last character in readonly mode
} else {
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
if app_state.current_position > total_count + 1 {
app_state.current_position = total_count + 1;
if app_state.current_position > current_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
form_state.reset_to_empty();
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
let current_position_to_load = app_state.current_position; // Use a copy
let load_message = UiService::load_adresar_by_position(
&mut grpc_client,
&mut app_state, // Pass app_state mutably if needed by the service
&mut form_state,
current_position_to_load
).await?;
current_position_to_load,
)
.await?;
let current_input = form_state.get_current_input();
let max_cursor_pos = if !event_handler.is_edit_mode && !current_input.is_empty() {
current_input.len() - 1 // In readonly mode, limit to last character
let max_cursor_pos = if !event_handler.is_edit_mode
&& !current_input.is_empty()
{
current_input.len() - 1 // In readonly mode, limit to last character
} else {
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
if !load_message.starts_with("Loaded entry") || message.is_empty() {
event_handler.command_message = load_message;
if !load_message.starts_with("Loaded entry")
|| event_handler.command_message.is_empty()
{
event_handler.command_message = load_message;
}
} else {
// 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
if app_state.current_position > total_count {
form_state.reset_to_empty();
form_state.current_field = 0;
}
app_state.current_position =
1.min(current_total_count + 1); // Go to 1 or new entry if empty
if app_state.current_position > total_count {
form_state.reset_to_empty();
form_state.current_field = 0;
}
}
} else if !position_changed && !event_handler.is_edit_mode {
// If position didn't change but we are in read-only, just adjust cursor
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);
}
} else if app_state.ui.show_login {
// Handle cursor updates for AuthState if needed, similar to FormState
if !event_handler.is_edit_mode {
let current_input = auth_state.get_current_input();
let max_cursor_pos = if !current_input.is_empty() {
current_input.len() - 1
} else {
0
};
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;
// Handle cursor updates for AuthState if needed, similar to FormState
if !event_handler.is_edit_mode {
let current_input = auth_state.get_current_input();
let max_cursor_pos = if !current_input.is_empty() {
current_input.len() - 1
} else {
0
};
auth_state.current_cursor_pos =
event_handler.ideal_cursor_column.min(max_cursor_pos);
}
}
// Check exit condition *after* processing outcome
if should_exit {
// terminal.cleanup()?; // Optional: Drop handles this
return Ok(());
}
}