diff --git a/Cargo.lock b/Cargo.lock index 6ea6dac..6251611 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,9 +109,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.97" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "arc-swap" @@ -423,6 +423,7 @@ dependencies = [ name = "client" version = "0.3.13" dependencies = [ + "anyhow", "async-trait", "common", "crossterm", diff --git a/client/Cargo.toml b/client/Cargo.toml index 295607a..54ba6d7 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -5,6 +5,7 @@ edition.workspace = true license.workspace = true [dependencies] +anyhow = "1.0.98" async-trait = "0.1.88" common = { path = "../common" } diff --git a/client/src/config/binds/config.rs b/client/src/config/binds/config.rs index 3d701c7..cd59041 100644 --- a/client/src/config/binds/config.rs +++ b/client/src/config/binds/config.rs @@ -3,6 +3,7 @@ use serde::Deserialize; use std::collections::HashMap; use std::path::Path; +use anyhow::{Context, Result}; use crossterm::event::{KeyCode, KeyModifiers}; #[derive(Debug, Deserialize, Default)] @@ -43,11 +44,11 @@ pub struct ModeKeybindings { impl Config { /// Loads the configuration from "config.toml" in the client crate directory. - pub fn load() -> Result> { + pub fn load() -> Result { let manifest_dir = env!("CARGO_MANIFEST_DIR"); let config_path = Path::new(manifest_dir).join("config.toml"); let config_str = std::fs::read_to_string(&config_path) - .map_err(|e| format!("Failed to read config file at {:?}: {}", config_path, e))?; + .with_context(|| format!("Failed to read config file at {:?}", config_path))?; let config: Config = toml::from_str(&config_str)?; Ok(config) } diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index 298926b..505f6fd 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -8,7 +8,7 @@ use crate::ui::handlers::rat_state::UiStateHandler; use crate::ui::handlers::context::UiContext; use crate::ui::handlers::context::DialogPurpose; use crate::functions::common::buffer; -use std::error::Error; +use anyhow::{Context, Result}; use crate::tui::{ terminal::core::TerminalCore, functions::{ @@ -68,7 +68,7 @@ pub struct EventHandler { } impl EventHandler { - pub async fn new(login_result_sender: mpsc::Sender) -> Result> { + pub async fn new(login_result_sender: mpsc::Sender) -> Result { Ok(EventHandler { command_mode: false, command_input: String::new(), @@ -100,7 +100,7 @@ impl EventHandler { app_state: &mut AppState, total_count: u64, current_position: &mut u64, - ) -> Result> { + ) -> Result { let current_mode = ModeManager::derive_mode(app_state, self); app_state.update_mode(current_mode); @@ -257,7 +257,9 @@ impl EventHandler { spawn(async move { let login_outcome = match AuthClient::new().await { Ok(mut auth_client) => { - match auth_client.login(username, password).await { + match auth_client.login(username.clone(), password).await + .with_context(|| format!("Spawned login task failed for identifier: {}", username)) + { Ok(response) => login::LoginResult::Success(response), Err(e) => login::LoginResult::Failure(format!("{}", e)), } diff --git a/client/src/services/auth.rs b/client/src/services/auth.rs index adcc3f4..8fff97b 100644 --- a/client/src/services/auth.rs +++ b/client/src/services/auth.rs @@ -5,20 +5,22 @@ use common::proto::multieko2::auth::{ LoginRequest, LoginResponse, RegisterRequest, AuthResponse, }; -use std::error::Error; +use anyhow::{Context, Result}; pub struct AuthClient { client: AuthServiceClient, } impl AuthClient { - pub async fn new() -> Result> { - let client = AuthServiceClient::connect("http://[::1]:50051").await?; + pub async fn new() -> Result { + let client = AuthServiceClient::connect("http://[::1]:50051") + .await + .context("Failed to connect to auth service")?; Ok(Self { client }) } /// Login user via gRPC. - pub async fn login(&mut self, identifier: String, password: String) -> Result> { + pub async fn login(&mut self, identifier: String, password: String) -> Result { let request = tonic::Request::new(LoginRequest { identifier, password }); let response = self.client.login(request).await?.into_inner(); Ok(response) @@ -32,7 +34,7 @@ impl AuthClient { password: Option, password_confirmation: Option, role: Option, - ) -> Result> { + ) -> Result { let request = tonic::Request::new(RegisterRequest { username, email, diff --git a/client/src/services/grpc_client.rs b/client/src/services/grpc_client.rs index dcee771..ec9cf9e 100644 --- a/client/src/services/grpc_client.rs +++ b/client/src/services/grpc_client.rs @@ -10,6 +10,7 @@ use common::proto::multieko2::table_definition::{ table_definition_client::TableDefinitionClient, ProfileTreeResponse }; +use anyhow::{Context, Result}; #[derive(Clone)] pub struct GrpcClient { @@ -19,7 +20,7 @@ pub struct GrpcClient { } impl GrpcClient { - pub async fn new() -> Result> { + pub async fn new() -> Result { let adresar_client = AdresarClient::connect("http://[::1]:50051").await?; let table_structure_client = TableStructureServiceClient::connect("http://[::1]:50051").await?; let table_definition_client = TableDefinitionClient::connect("http://[::1]:50051").await?; @@ -31,37 +32,37 @@ impl GrpcClient { }) } - pub async fn get_adresar_count(&mut self) -> Result> { + pub async fn get_adresar_count(&mut self) -> Result { let request = tonic::Request::new(Empty::default()); let response: CountResponse = self.adresar_client.get_adresar_count(request).await?.into_inner(); Ok(response.count as u64) } - pub async fn get_adresar_by_position(&mut self, position: u64) -> Result> { + pub async fn get_adresar_by_position(&mut self, position: u64) -> Result { let request = tonic::Request::new(PositionRequest { position: position as i64 }); let response: AdresarResponse = self.adresar_client.get_adresar_by_position(request).await?.into_inner(); Ok(response) } - pub async fn post_adresar(&mut self, request: PostAdresarRequest) -> Result, Box> { + pub async fn post_adresar(&mut self, request: PostAdresarRequest) -> Result> { let request = tonic::Request::new(request); let response = self.adresar_client.post_adresar(request).await?; Ok(response) } - pub async fn put_adresar(&mut self, request: PutAdresarRequest) -> Result, Box> { + pub async fn put_adresar(&mut self, request: PutAdresarRequest) -> Result> { let request = tonic::Request::new(request); let response = self.adresar_client.put_adresar(request).await?; Ok(response) } - pub async fn get_table_structure(&mut self) -> Result> { + pub async fn get_table_structure(&mut self) -> Result { let request = tonic::Request::new(Empty::default()); let response = self.table_structure_client.get_adresar_table_structure(request).await?; Ok(response.into_inner()) } - pub async fn get_profile_tree(&mut self) -> Result> { + pub async fn get_profile_tree(&mut self) -> Result { let request = tonic::Request::new(Empty::default()); let response = self.table_definition_client.get_profile_tree(request).await?; Ok(response.into_inner()) diff --git a/client/src/services/ui_service.rs b/client/src/services/ui_service.rs index 375e669..b6680a5 100644 --- a/client/src/services/ui_service.rs +++ b/client/src/services/ui_service.rs @@ -4,6 +4,7 @@ use crate::services::grpc_client::GrpcClient; use crate::state::pages::form::FormState; use crate::tui::functions::common::form::SaveOutcome; use crate::state::app::state::AppState; +use anyhow::{Context, Result}; pub struct UiService; @@ -13,7 +14,7 @@ impl UiService { app_state: &mut AppState, ) -> Result, Box> { // Fetch profile tree - let profile_tree = grpc_client.get_profile_tree().await?; + let profile_tree = grpc_client.get_profile_tree().await.context("Failed to get profile tree")?; app_state.profile_tree = profile_tree; // Fetch table structure @@ -32,8 +33,8 @@ impl UiService { pub async fn initialize_adresar_count( grpc_client: &mut GrpcClient, app_state: &mut AppState, - ) -> Result<(), Box> { - let total_count = grpc_client.get_adresar_count().await?; + ) -> Result<()> { + let total_count = grpc_client.get_adresar_count().await.await.context("Failed to get adresar count")?; app_state.update_total_count(total_count); app_state.update_current_position(total_count.saturating_add(1)); // Start in new entry mode Ok(()) @@ -42,8 +43,8 @@ impl UiService { pub async fn update_adresar_count( grpc_client: &mut GrpcClient, app_state: &mut AppState, - ) -> Result<(), Box> { - let total_count = grpc_client.get_adresar_count().await?; + ) -> Result<()> { + let total_count = grpc_client.get_adresar_count().await.context("Failed to get adresar by position")?; app_state.update_total_count(total_count); Ok(()) } @@ -53,7 +54,7 @@ impl UiService { _app_state: &mut AppState, form_state: &mut FormState, position: u64, - ) -> Result> { + ) -> Result { match grpc_client.get_adresar_by_position(position).await { Ok(response) => { // Set the ID properly @@ -92,8 +93,8 @@ impl UiService { save_outcome: SaveOutcome, grpc_client: &mut GrpcClient, app_state: &mut AppState, - form_state: &mut FormState, // Needed to potentially update position/ID - ) -> Result<(), Box> { + form_state: &mut FormState, + ) -> Result<()> { match save_outcome { SaveOutcome::CreatedNew(new_id) => { // A new record was created, update the count! diff --git a/client/src/state/app/state.rs b/client/src/state/app/state.rs index aa3d70a..d736804 100644 --- a/client/src/state/app/state.rs +++ b/client/src/state/app/state.rs @@ -4,6 +4,7 @@ use std::env; use common::proto::multieko2::table_definition::ProfileTreeResponse; use crate::modes::handlers::mode_manager::AppMode; use crate::ui::handlers::context::DialogPurpose; +use anyhow::{Context, Result}; pub struct DialogState { pub dialog_show: bool, @@ -43,7 +44,7 @@ pub struct AppState { } impl AppState { - pub fn new() -> Result> { + pub fn new() -> Result { let current_dir = env::current_dir()? .to_string_lossy() .to_string(); diff --git a/client/src/tui/functions/common/login.rs b/client/src/tui/functions/common/login.rs index 534660b..fd8fbf9 100644 --- a/client/src/tui/functions/common/login.rs +++ b/client/src/tui/functions/common/login.rs @@ -9,6 +9,7 @@ use crate::state::pages::canvas_state::CanvasState; use crate::ui::handlers::context::DialogPurpose; use crate::modes::handlers::event::EventOutcome; use common::proto::multieko2::auth::LoginResponse; +use anyhow::{Context, Result}; #[derive(Debug)] pub enum LoginResult { @@ -19,13 +20,12 @@ pub enum LoginResult { /// Attempts to log the user in using the provided credentials via gRPC. /// Updates AuthState and AppState on success or failure. -/// (This is your existing function - remains unchanged) pub async fn save( auth_state: &mut AuthState, login_state: &mut LoginState, auth_client: &mut AuthClient, app_state: &mut AppState, -) -> Result> { +) -> Result { let identifier = login_state.username.clone(); let password = login_state.password.clone(); @@ -40,7 +40,7 @@ pub async fn save( DialogPurpose::LoginFailed, ); login_state.error_message = Some(error_message.clone()); - return Ok(error_message); + return Err(anyhow::anyhow!(error_message)); } // Clear previous error/dialog state before attempting @@ -48,7 +48,9 @@ pub async fn save( app_state.hide_dialog(); // Hide any previous dialog // Call the gRPC login method - match auth_client.login(identifier, password).await { + match auth_client.login(identifier.clone(), password).await + .with_context(|| format!("gRPC login attempt failed for identifier: {}", identifier)) + { Ok(response) => { // Store authentication details using correct field names auth_state.auth_token = Some(response.access_token.clone()); @@ -92,28 +94,11 @@ pub async fn save( login_state.set_has_unsaved_changes(true); login_state.username.clear(); login_state.password.clear(); - Ok(format!("Login failed: {}", error_message)) + Err(e) } } } -// --- Add this new function --- -/// Sets the stage for login: shows loading dialog and sets the pending flag. -/// Call this from the event handler when login is triggered. -pub async fn initiate_login( - app_state: &mut AppState, - login_state: &mut LoginState, -) -> Result> { - // Show the loading dialog immediately - app_state.show_loading_dialog("Logging In", "Please wait..."); - // Set the flag in LoginState to indicate the actual save should run next loop - login_state.login_request_pending = true; - // Return immediately to allow redraw - Ok(EventOutcome::Ok("Login initiated.".to_string())) -} -// --- End of new function --- - - /// Reverts the login form fields to empty and returns to the previous screen (Intro). pub async fn revert( login_state: &mut LoginState, diff --git a/client/src/tui/terminal/core.rs b/client/src/tui/terminal/core.rs index e0c1e6d..e7c5ee0 100644 --- a/client/src/tui/terminal/core.rs +++ b/client/src/tui/terminal/core.rs @@ -7,13 +7,14 @@ use crossterm::{ }; use ratatui::{backend::CrosstermBackend, Terminal}; use std::io::{self, stdout, Write}; +use anyhow::{Context, Result}; pub struct TerminalCore { terminal: Terminal>, } impl TerminalCore { - pub fn new() -> Result> { + pub fn new() -> Result { enable_raw_mode()?; let mut stdout = stdout(); execute!( @@ -27,7 +28,7 @@ impl TerminalCore { Ok(Self { terminal }) } - pub fn draw(&mut self, f: F) -> Result<(), Box> + pub fn draw(&mut self, f: F) -> Result<()> where F: FnOnce(&mut ratatui::Frame), { @@ -35,7 +36,7 @@ impl TerminalCore { Ok(()) } - pub fn cleanup(&mut self) -> Result<(), Box> { + pub fn cleanup(&mut self) -> Result<()> { let backend = self.terminal.backend_mut(); execute!( backend, @@ -56,7 +57,7 @@ impl TerminalCore { pub fn set_cursor_style( &mut self, style: SetCursorStyle, - ) -> Result<(), Box> { + ) -> Result<()> { execute!( self.terminal.backend_mut(), style, @@ -65,7 +66,7 @@ impl TerminalCore { Ok(()) } - pub fn show_cursor(&mut self) -> Result<(), Box> { + pub fn show_cursor(&mut self) -> Result<()> { execute!( self.terminal.backend_mut(), Show @@ -73,7 +74,7 @@ impl TerminalCore { Ok(()) } - pub fn hide_cursor(&mut self) -> Result<(), Box> { + pub fn hide_cursor(&mut self) -> Result<()> { execute!( self.terminal.backend_mut(), Hide diff --git a/client/src/tui/terminal/event_reader.rs b/client/src/tui/terminal/event_reader.rs index 99c190a..22c5bef 100644 --- a/client/src/tui/terminal/event_reader.rs +++ b/client/src/tui/terminal/event_reader.rs @@ -1,6 +1,7 @@ // src/tui/terminal/event_reader.rs use crossterm::event::{self, Event}; +use anyhow::{Context, Result}; pub struct EventReader; @@ -9,7 +10,7 @@ impl EventReader { Self } - pub fn read_event(&self) -> Result> { + pub fn read_event(&self) -> Result { Ok(event::read()?) } } diff --git a/client/src/ui/handlers/ui.rs b/client/src/ui/handlers/ui.rs index 3065701..b50b21b 100644 --- a/client/src/ui/handlers/ui.rs +++ b/client/src/ui/handlers/ui.rs @@ -18,30 +18,28 @@ use crate::state::pages::intro::IntroState; use crate::state::app::buffer::BufferState; use crate::state::app::buffer::AppView; use crate::state::app::state::AppState; -use crate::ui::handlers::context::DialogPurpose; // <-- Add DialogPurpose import -// Import SaveOutcome +use crate::ui::handlers::context::DialogPurpose; use crate::tui::terminal::{EventReader, TerminalCore}; use crate::ui::handlers::render::render_ui; -use crate::tui::functions::common::login; use crate::tui::functions::common::login::LoginResult; use std::time::Instant; -use std::error::Error; +use anyhow::{Context, Result}; use crossterm::cursor::SetCursorStyle; use crossterm::event as crossterm_event; use tracing::{info, error}; use tokio::sync::mpsc; -pub async fn run_ui() -> Result<(), Box> { - let config = Config::load()?; +pub async fn run_ui() -> Result<()> { + let config = Config::load().context("Failed to load configuration")?; let theme = Theme::from_str(&config.colors.theme); - let mut terminal = TerminalCore::new()?; + let mut terminal = TerminalCore::new().context("Failed to initialize terminal")?; let mut grpc_client = GrpcClient::new().await?; let mut command_handler = CommandHandler::new(); // --- Channel for Login Results --- let (login_result_sender, mut login_result_receiver) = mpsc::channel::(1); - let mut event_handler = EventHandler::new(login_result_sender.clone()).await?; + let mut event_handler = EventHandler::new(login_result_sender.clone()).await.context("Failed to create event handler")?; let event_reader = EventReader::new(); let mut auth_state = AuthState::default(); @@ -50,16 +48,16 @@ pub async fn run_ui() -> Result<(), Box> { let mut intro_state = IntroState::default(); let mut admin_state = AdminState::default(); let mut buffer_state = BufferState::default(); - let mut app_state = AppState::new()?; + let mut app_state = AppState::new().context("Failed to create initial app state")?; // Initialize app state with profile tree and table structure let column_names = UiService::initialize_app_state(&mut grpc_client, &mut app_state) - .await?; + .await.context("Failed to initialize app state from UI service")?; let mut form_state = FormState::new(column_names); // Fetch the total count of Adresar entries - UiService::initialize_adresar_count(&mut grpc_client, &mut app_state) + UiService::initialize_adresar_count(&mut grpc_client, &mut app_state).await .await?; form_state.reset_to_empty(); @@ -121,7 +119,7 @@ pub async fn run_ui() -> Result<(), Box> { current_fps, &app_state, ); - })?; + }).context("Terminal draw call failed")?; // --- Cursor Visibility Logic --- // (Keep existing cursor logic here - depends on state drawn above) @@ -132,13 +130,13 @@ pub async fn run_ui() -> Result<(), Box> { 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()?; + terminal.show_cursor().context("Failed to show cursor in ReadOnly mode")?; } 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()?; } + AppMode::Command => { terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; terminal.show_cursor().context("Failed to show cursor in Command mode")?; } } // --- End Cursor Visibility Logic --- @@ -150,7 +148,7 @@ pub async fn run_ui() -> Result<(), Box> { let mut event_outcome_result = Ok(EventOutcome::Ok(String::new())); // Poll for events *after* drawing and checking pending actions if crossterm_event::poll(std::time::Duration::from_millis(20))? { - let event = event_reader.read_event()?; + let event = event_reader.read_event().context("Failed to read terminal event")?; event_outcome_result = event_handler .handle_event( event, @@ -308,7 +306,7 @@ pub async fn run_ui() -> Result<(), Box> { &mut form_state, current_position_to_load, ) - .await?; + .await.with_context(|| format!("Failed to load adresar by position: {}", current_position_to_load))?; let current_input = form_state.get_current_input(); let max_cursor_pos = if !event_handler.is_edit_mode