diff --git a/client/src/modes/handlers/command_mode.rs b/client/src/modes/handlers/command_mode.rs index f1ecb75..05b6763 100644 --- a/client/src/modes/handlers/command_mode.rs +++ b/client/src/modes/handlers/command_mode.rs @@ -1,7 +1,11 @@ // src/modes/handlers/command_mode.rs use crossterm::event::{KeyEvent, KeyCode, KeyModifiers}; -use crate::tui::terminal::AppTerminal; +use crate::tui::terminal::{ + core::TerminalCore, + grpc_client::GrpcClient, + commands::CommandHandler, +}; use crate::config::config::Config; use crate::ui::handlers::form::FormState; use super::common; @@ -12,7 +16,7 @@ pub async fn handle_command_event( form_state: &mut FormState, command_input: &mut String, command_message: &mut String, - app_terminal: &mut AppTerminal, + grpc_client: &mut GrpcClient, is_saved: &mut bool, current_position: &mut u64, total_count: u64, @@ -65,7 +69,7 @@ async fn process_command( form_state: &mut FormState, command_input: &mut String, command_message: &mut String, - app_terminal: &mut AppTerminal, + grpc_client: &mut GrpcClient, is_saved: &mut bool, current_position: &mut u64, total_count: u64, diff --git a/client/src/modes/handlers/common.rs b/client/src/modes/handlers/common.rs index 73ca5e8..40555c9 100644 --- a/client/src/modes/handlers/common.rs +++ b/client/src/modes/handlers/common.rs @@ -1,6 +1,10 @@ // src/modes/handlers/common.rs -use crate::tui::terminal::AppTerminal; +use crate::tui::terminal::{ + core::TerminalCore, + grpc_client::GrpcClient, + commands::CommandHandler, +}; use crate::ui::handlers::form::FormState; use common::proto::multieko2::adresar::{PostAdresarRequest, PutAdresarRequest}; diff --git a/client/src/modes/handlers/edit.rs b/client/src/modes/handlers/edit.rs index 2f39654..0013415 100644 --- a/client/src/modes/handlers/edit.rs +++ b/client/src/modes/handlers/edit.rs @@ -1,7 +1,11 @@ // src/modes/handlers/edit.rs use crossterm::event::{KeyEvent, KeyCode, KeyModifiers}; -use crate::tui::terminal::AppTerminal; +use crate::tui::terminal::{ + core::TerminalCore, + grpc_client::GrpcClient, + commands::CommandHandler, +}; use crate::config::config::Config; use crate::ui::handlers::form::FormState; use super::common; @@ -13,7 +17,7 @@ pub async fn handle_edit_event_internal( form_state: &mut FormState, ideal_cursor_column: &mut usize, command_message: &mut String, - app_terminal: &mut AppTerminal, // Add these parameters + terminal: &mut TerminalCore, is_saved: &mut bool, current_position: &mut u64, total_count: u64, diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index 78a75ff..db07859 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -2,7 +2,11 @@ use crossterm::event::Event; use crossterm::cursor::SetCursorStyle; -use crate::tui::terminal::AppTerminal; +use crate::tui::terminal::{ + core::TerminalCore, + grpc_client::GrpcClient, + commands::CommandHandler, +}; use crate::config::config::Config; use crate::ui::handlers::form::FormState; use crate::modes::handlers::{edit, command_mode, read_only}; @@ -36,7 +40,9 @@ impl EventHandler { &mut self, event: Event, config: &Config, - app_terminal: &mut AppTerminal, + terminal: &mut TerminalCore, + grpc_client: &mut GrpcClient, + command_handler: &mut CommandHandler, form_state: &mut FormState, is_saved: &mut bool, total_count: u64, @@ -52,7 +58,7 @@ impl EventHandler { "save" => { let message = common::save( form_state, - app_terminal, + grpc_client, is_saved, current_position, total_count, @@ -60,22 +66,17 @@ impl EventHandler { return Ok((false, message)); }, "force_quit" => { - let (should_exit, message) = common::force_quit(); + let (should_exit, message) = command_handler.handle_command("force_quit", terminal).await?; return Ok((should_exit, message)); }, "save_and_quit" => { - let (should_exit, message) = common::save_and_quit( - form_state, - app_terminal, - current_position, - total_count, - ).await?; + let (should_exit, message) = command_handler.handle_command("save_and_quit", terminal).await?; return Ok((should_exit, message)); }, "revert" => { let message = common::revert( form_state, - app_terminal, + grpc_client, current_position, total_count, ).await?; @@ -93,7 +94,7 @@ impl EventHandler { form_state, &mut self.command_input, &mut self.command_message, - app_terminal, + grpc_client, is_saved, current_position, total_count, @@ -124,7 +125,7 @@ impl EventHandler { self.is_edit_mode = false; self.edit_mode_cooldown = true; self.command_message = "Read-only mode".to_string(); - app_terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?; + terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?; let current_input = form_state.get_current_input(); if !current_input.is_empty() && form_state.current_cursor_pos >= current_input.len() { @@ -141,7 +142,8 @@ impl EventHandler { form_state, &mut self.ideal_cursor_column, &mut self.command_message, - app_terminal, + terminal, + grpc_client, is_saved, current_position, total_count, @@ -152,7 +154,6 @@ impl EventHandler { } else { // In READ-ONLY mode, we DO want to check for entering command mode // Check for entering command mode (only in read-only mode) - // Use get_read_only_action_for_key instead of is_enter_command_mode if let Some(action) = config.get_read_only_action_for_key(key.code, key.modifiers) { if action == "enter_command_mode" { self.command_mode = true; @@ -167,10 +168,10 @@ impl EventHandler { self.is_edit_mode = true; self.edit_mode_cooldown = true; self.command_message = "Edit mode".to_string(); - app_terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?; + terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?; return Ok((false, self.command_message.clone())); } - + if config.is_enter_edit_mode_after(key.code, key.modifiers) { let current_input = form_state.get_current_input(); if !current_input.is_empty() && form_state.current_cursor_pos < current_input.len() { @@ -180,7 +181,7 @@ impl EventHandler { self.is_edit_mode = true; self.edit_mode_cooldown = true; self.command_message = "Edit mode (after cursor)".to_string(); - app_terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?; + terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?; return Ok((false, self.command_message.clone())); } @@ -192,7 +193,8 @@ impl EventHandler { &mut self.key_sequence_tracker, current_position, total_count, - app_terminal, + terminal, + grpc_client, &mut self.command_message, &mut self.edit_mode_cooldown, &mut self.ideal_cursor_column, diff --git a/client/src/modes/handlers/read_only.rs b/client/src/modes/handlers/read_only.rs index 7753364..3a989f2 100644 --- a/client/src/modes/handlers/read_only.rs +++ b/client/src/modes/handlers/read_only.rs @@ -4,7 +4,11 @@ use crossterm::event::{KeyEvent}; use crate::config::config::Config; use crate::ui::handlers::form::FormState; use crate::config::key_sequences::KeySequenceTracker; -use crate::tui::terminal::AppTerminal; +use crate::tui::terminal::{ + core::TerminalCore, + grpc_client::GrpcClient, + commands::CommandHandler, +}; #[derive(PartialEq)] enum CharType { diff --git a/client/src/modes/handlerscommand_mode.rs b/client/src/modes/handlerscommand_mode.rs new file mode 100644 index 0000000..e69de29 diff --git a/client/src/tui/terminal.rs b/client/src/tui/terminal.rs index c4c6982..5012c64 100644 --- a/client/src/tui/terminal.rs +++ b/client/src/tui/terminal.rs @@ -1,174 +1,11 @@ -// src/client/terminal.rs -use crossterm::event::{self, Event}; -use crossterm::{ - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen}, -}; -use crossterm::cursor::{SetCursorStyle, EnableBlinking}; -use ratatui::{backend::CrosstermBackend, Terminal}; -use std::io::{self, stdout, Write}; -use tonic::transport::Channel; -use ratatui::backend::Backend; +// src/tui/terminal.rs -// Import the correct clients and proto messages from their respective modules -use common::proto::multieko2::adresar::adresar_client::AdresarClient; -use common::proto::multieko2::adresar::{AdresarResponse, PostAdresarRequest, PutAdresarRequest}; -use common::proto::multieko2::common::{CountResponse, PositionRequest, Empty}; -use common::proto::multieko2::table_structure::table_structure_service_client::TableStructureServiceClient; -use common::proto::multieko2::table_structure::TableStructureResponse; +pub mod core; +pub mod grpc_client; +pub mod commands; +pub mod events; -pub struct AppTerminal { - terminal: Terminal>, - adresar_client: AdresarClient, - table_structure_client: TableStructureServiceClient, -} - -impl Drop for AppTerminal { - fn drop(&mut self) { - let _ = self.cleanup(); // Best-effort cleanup during drop - } -} - -impl AppTerminal { - pub fn set_cursor_style( - &mut self, - style: SetCursorStyle, - ) -> Result<(), Box> { - execute!( - self.terminal.backend_mut(), - style, - EnableBlinking, - )?; - Ok(()) - } - - pub async fn new() -> Result> { - enable_raw_mode()?; - let mut stdout = stdout(); - execute!( - stdout, - EnterAlternateScreen, - SetCursorStyle::SteadyBlock - )?; - let backend = CrosstermBackend::new(stdout); - let terminal = Terminal::new(backend)?; - - // Initialize both gRPC clients - let adresar_client = AdresarClient::connect("http://[::1]:50051").await?; - let table_structure_client = TableStructureServiceClient::connect("http://[::1]:50051").await?; - - Ok(Self { terminal, adresar_client, table_structure_client }) - } - - pub fn draw(&mut self, f: F) -> Result<(), Box> - where - F: FnOnce(&mut ratatui::Frame), - { - self.terminal.draw(f)?; - Ok(()) - } - - pub fn read_event(&self) -> Result> { - Ok(event::read()?) - } - - pub fn cleanup(&mut self) -> Result<(), Box> { - // Get a separate stdout handle for cleanup operations - let mut stdout = stdout(); - - // Step 1: Show cursor first (most important for user experience) - execute!(stdout, crossterm::cursor::Show)?; - - // Step 2: Reset cursor style to default - execute!(stdout, crossterm::cursor::SetCursorStyle::DefaultUserShape)?; - - // Step 3: Leave alternate screen mode - execute!(stdout, crossterm::terminal::LeaveAlternateScreen)?; - - // Step 4: Disable raw mode - disable_raw_mode()?; - - // Step 5: Flush all pending changes to ensure they're applied - stdout.flush()?; - - // Step 6: Final reset - clear screen and move cursor to home position - // This ensures terminal is in a known good state - execute!( - stdout, - crossterm::terminal::Clear(crossterm::terminal::ClearType::All), - crossterm::cursor::MoveTo(0, 0) - )?; - - Ok(()) - } - - pub async fn handle_command( - &mut self, - action: &str, - is_saved: &mut bool, - ) -> Result<(bool, String), Box> { - match action { - "quit" => { - if *is_saved { - self.cleanup()?; - Ok((true, "Exiting.".to_string())) - } else { - Ok((false, "No changes saved. Use :q! to force quit.".to_string())) - } - } - "force_quit" => { - self.cleanup()?; - Ok((true, "Force exiting without saving.".to_string())) - } - "save_and_quit" => { - *is_saved = true; - self.cleanup()?; - Ok((true, "State saved. Exiting.".to_string())) - } - _ => Ok((false, format!("Action not recognized: {}", action))), - } - } - - - // Adresar service methods use adresar_client - pub async fn get_adresar_count(&mut self) -> Result> { - let request = tonic::Request::new(Empty::default()); - let response: CountResponse = self.adresar_client.get_adresar_count(request).await?.into_inner(); - Ok(response.count as u64) - } - - pub async fn get_adresar_by_position(&mut self, position: u64) -> Result> { - let request = tonic::Request::new(PositionRequest { position: position as i64 }); - let response: AdresarResponse = self.adresar_client.get_adresar_by_position(request).await?.into_inner(); - Ok(response) - } - - pub async fn post_adresar( - &mut self, - request: PostAdresarRequest, - ) -> Result, Box> { - 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> { - let request = tonic::Request::new(request); - let response = self.adresar_client.put_adresar(request).await?; - Ok(response) - } - - // Table structure method uses table_structure_client - 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 use core::TerminalCore; +pub use grpc_client::GrpcClient; +pub use commands::CommandHandler; +pub use events::EventHandler; diff --git a/client/src/tui/terminal/commands.rs b/client/src/tui/terminal/commands.rs new file mode 100644 index 0000000..6648ed2 --- /dev/null +++ b/client/src/tui/terminal/commands.rs @@ -0,0 +1,48 @@ +// src/tui/terminal/commands.rs + +use crate::tui::terminal::core::TerminalCore; +use crate::tui::terminal::grpc_client::GrpcClient; + +pub struct CommandHandler { + grpc_client: GrpcClient, + is_saved: bool, +} + +impl CommandHandler { + pub fn new(grpc_client: GrpcClient) -> Self { + Self { grpc_client, is_saved: false } + } + + pub async fn handle_command( + &mut self, + action: &str, + terminal: &mut TerminalCore + ) -> Result<(bool, String), Box> { + match action { + "quit" => self.handle_quit(terminal).await, + "force_quit" => self.handle_force_quit(terminal).await, + "save_and_quit" => self.handle_save_quit(terminal).await, + _ => Ok((false, format!("Unknown command: {}", action))), + } + } + + async fn handle_quit(&self, terminal: &mut TerminalCore) -> Result<(bool, String), Box> { + if self.is_saved { + terminal.cleanup()?; + Ok((true, "Exiting.".into())) + } else { + Ok((false, "No changes saved. Use :q! to force quit.".into())) + } + } + + async fn handle_force_quit(&self, terminal: &mut TerminalCore) -> Result<(bool, String), Box> { + terminal.cleanup()?; + Ok((true, "Force exiting without saving.".into())) + } + + async fn handle_save_quit(&mut self, terminal: &mut TerminalCore) -> Result<(bool, String), Box> { + self.is_saved = true; + terminal.cleanup()?; + Ok((true, "State saved. Exiting.".into())) + } +} diff --git a/client/src/tui/terminal/core.rs b/client/src/tui/terminal/core.rs new file mode 100644 index 0000000..42f050b --- /dev/null +++ b/client/src/tui/terminal/core.rs @@ -0,0 +1,65 @@ +// src/tui/terminal/core.rs + +use crossterm::{ + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + cursor::{SetCursorStyle, EnableBlinking, Show, MoveTo}, + event::Event, +}; +use ratatui::{backend::CrosstermBackend, Terminal}; +use std::io::{self, stdout, Write}; + +pub struct TerminalCore { + terminal: Terminal>, +} + +impl TerminalCore { + pub fn new() -> Result> { + enable_raw_mode()?; + let mut stdout = stdout(); + execute!( + stdout, + EnterAlternateScreen, + SetCursorStyle::SteadyBlock, + EnableBlinking + )?; + let backend = CrosstermBackend::new(stdout); + let terminal = Terminal::new(backend)?; + Ok(Self { terminal }) + } + + pub fn draw(&mut self, f: F) -> Result<(), Box> + where + F: FnOnce(&mut ratatui::Frame), + { + self.terminal.draw(f)?; + Ok(()) + } + + pub fn cleanup(&mut self) -> Result<(), Box> { + let mut stdout = stdout(); + execute!(stdout, Show)?; + execute!(stdout, SetCursorStyle::DefaultUserShape)?; + execute!(stdout, LeaveAlternateScreen)?; + disable_raw_mode()?; + stdout.flush()?; + execute!( + stdout, + crossterm::terminal::Clear(crossterm::terminal::ClearType::All), + MoveTo(0, 0) + )?; + Ok(()) + } + + pub fn set_cursor_style( + &mut self, + style: SetCursorStyle, + ) -> Result<(), Box> { + execute!( + self.terminal.backend_mut(), + style, + EnableBlinking, + )?; + Ok(()) + } +} diff --git a/client/src/tui/terminal/events.rs b/client/src/tui/terminal/events.rs new file mode 100644 index 0000000..9a4be53 --- /dev/null +++ b/client/src/tui/terminal/events.rs @@ -0,0 +1,15 @@ +// src/tui/terminal/events.rs + +use crossterm::event::{self, Event}; + +pub struct EventHandler; + +impl EventHandler { + pub fn new() -> Self { + Self + } + + pub fn read_event(&self) -> Result> { + Ok(event::read()?) + } +} diff --git a/client/src/tui/terminal/grpc_client.rs b/client/src/tui/terminal/grpc_client.rs new file mode 100644 index 0000000..8862c00 --- /dev/null +++ b/client/src/tui/terminal/grpc_client.rs @@ -0,0 +1,51 @@ +// src/tui/terminal/grpc_client.rs + +use tonic::transport::Channel; +use common::proto::multieko2::adresar::adresar_client::AdresarClient; +use common::proto::multieko2::adresar::{AdresarResponse, PostAdresarRequest, PutAdresarRequest}; +use common::proto::multieko2::common::{CountResponse, PositionRequest, Empty}; +use common::proto::multieko2::table_structure::table_structure_service_client::TableStructureServiceClient; +use common::proto::multieko2::table_structure::TableStructureResponse; + +pub struct GrpcClient { + adresar_client: AdresarClient, + table_structure_client: TableStructureServiceClient, +} + +impl GrpcClient { + pub async fn new() -> Result> { + let adresar_client = AdresarClient::connect("http://[::1]:50051").await?; + let table_structure_client = TableStructureServiceClient::connect("http://[::1]:50051").await?; + Ok(Self { adresar_client, table_structure_client }) + } + + pub async fn get_adresar_count(&mut self) -> Result> { + let request = tonic::Request::new(Empty::default()); + let response: CountResponse = self.adresar_client.get_adresar_count(request).await?.into_inner(); + Ok(response.count as u64) + } + + pub async fn get_adresar_by_position(&mut self, position: u64) -> Result> { + let request = tonic::Request::new(PositionRequest { position: position as i64 }); + let response: AdresarResponse = self.adresar_client.get_adresar_by_position(request).await?.into_inner(); + Ok(response) + } + + pub async fn post_adresar(&mut self, request: PostAdresarRequest) -> Result, Box> { + 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> { + 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> { + let request = tonic::Request::new(Empty::default()); + let response = self.table_structure_client.get_adresar_table_structure(request).await?; + Ok(response.into_inner()) + } +} diff --git a/client/src/ui/handlers/ui.rs b/client/src/ui/handlers/ui.rs index 0ddff53..f47dc2b 100644 --- a/client/src/ui/handlers/ui.rs +++ b/client/src/ui/handlers/ui.rs @@ -1,6 +1,8 @@ // src/client/ui/handlers/ui.rs -use crate::tui::terminal::AppTerminal; +use crate::tui::terminal::TerminalCore; +use crate::tui::terminal::GrpcClient; +use crate::tui::terminal::CommandHandler; use crate::config::colors::Theme; use crate::config::config::Config; use crate::ui::handlers::{form::FormState, render::render_ui}; @@ -10,12 +12,14 @@ use crate::state::state::AppState; pub async fn run_ui() -> Result<(), Box> { let config = Config::load()?; - let mut app_terminal = AppTerminal::new().await?; + let mut terminal = TerminalCore::new()?; // Remove .await + let mut grpc_client = GrpcClient::new().await?; + let mut command_handler = CommandHandler::new(grpc_client); let theme = Theme::from_str(&config.colors.theme); // Fetch table structure at startup (one-time) // TODO: Later, consider implementing a live update for table structure changes. - let table_structure = app_terminal.get_table_structure().await?; + let table_structure = grpc_client.get_table_structure().await?; // Changed // Extract the column names from the response let column_names: Vec = table_structure @@ -32,16 +36,16 @@ pub async fn run_ui() -> Result<(), Box> { let mut app_state = AppState::new()?; // Fetch the total count of Adresar entries - let total_count = app_terminal.get_adresar_count().await?; + let total_count = grpc_client.get_adresar_count().await?; app_state.update_total_count(total_count); app_state.update_current_position(total_count.saturating_add(1)); // Start in new entry mode form_state.reset_to_empty(); loop { - let total_count = app_terminal.get_adresar_count().await?; + let total_count = grpc_client.get_adresar_count().await?; app_state.update_total_count(total_count); - app_terminal.draw(|f| { + terminal.draw(|f| { render_ui( f, &mut form_state, @@ -56,11 +60,13 @@ pub async fn run_ui() -> Result<(), Box> { ); })?; - let event = app_terminal.read_event()?; + let event = event_handler.read_event()?; let (should_exit, message) = event_handler.handle_event( event, &config, - &mut app_terminal, + &mut terminal, + &mut grpc_client, + &mut command_handler, &mut form_state, &mut app_state.is_saved, app_state.total_count, @@ -88,7 +94,7 @@ pub async fn run_ui() -> Result<(), Box> { form_state.current_field = 0; } else if app_state.current_position >= 1 && app_state.current_position <= total_count { // Existing entry - load data - match app_terminal.get_adresar_by_position(app_state.current_position).await { + match grpc_client.get_adresar_by_position(app_state.current_position).await { Ok(response) => { // Set the ID properly form_state.id = response.id;