diff --git a/Cargo.lock b/Cargo.lock index 327c4f7..079904a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1374,6 +1374,7 @@ dependencies = [ "serde_with", "sqlx", "tokio", + "toml", "tonic", "tonic-build", "tracing", @@ -1955,6 +1956,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2577,6 +2587,40 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +dependencies = [ + "indexmap 2.7.1", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tonic" version = "0.12.3" @@ -3117,6 +3161,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59690dea168f2198d1a3b0cac23b8063efcd11012f10ae4698f284808c8ef603" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen-rt" version = "0.33.0" diff --git a/Cargo.toml b/Cargo.toml index f6d59d8..9b52bce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ serde_json = "1.0.138" serde_with = "3.12.0" sqlx = { version = "0.8.3", features = ["postgres", "runtime-tokio", "runtime-tokio-native-tls", "time"] } tokio = { version = "1.43.0", features = ["full", "macros"] } +toml = "0.8.20" tonic = "0.12.3" tonic-build = "0.12.3" tracing = "0.1.41" diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..79e5157 --- /dev/null +++ b/config.toml @@ -0,0 +1,6 @@ +# config.toml +[keybindings] +save = [":w", "ctrl+s"] +quit = [":q", "ctrl+q"] +force_quit = [":q!", "ctrl+shift+q"] +save_and_quit = [":wq", "ctrl+shift+s"] diff --git a/src/client/config.rs b/src/client/config.rs new file mode 100644 index 0000000..e737efe --- /dev/null +++ b/src/client/config.rs @@ -0,0 +1,53 @@ +// src/client/config.rs +use serde::Deserialize; +use std::collections::HashMap; +use crossterm::event::{KeyCode, KeyModifiers}; + +#[derive(Debug, Deserialize)] +pub struct Config { + pub keybindings: HashMap>, +} + +impl Config { + pub fn load() -> Result> { + let config_str = std::fs::read_to_string("config.toml")?; + let config: Config = toml::from_str(&config_str)?; + Ok(config) + } + + pub fn get_action_for_key(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> { + for (action, bindings) in &self.keybindings { + for binding in bindings { + if Self::matches_keybinding(binding, key, modifiers) { + return Some(action); + } + } + } + None + } + + fn matches_keybinding(binding: &str, key: KeyCode, modifiers: KeyModifiers) -> bool { + let parts: Vec<&str> = binding.split('+').collect(); + let mut expected_modifiers = KeyModifiers::empty(); + let mut expected_key = None; + + for part in parts { + match part.to_lowercase().as_str() { + "ctrl" => expected_modifiers |= KeyModifiers::CONTROL, + "shift" => expected_modifiers |= KeyModifiers::SHIFT, + "alt" => expected_modifiers |= KeyModifiers::ALT, + _ => { + expected_key = match part.to_lowercase().as_str() { + "s" => Some(KeyCode::Char('s')), + "q" => Some(KeyCode::Char('q')), + "w" => Some(KeyCode::Char('w')), + ":" => Some(KeyCode::Char(':')), + _ => None, + }; + } + } + } + + modifiers == expected_modifiers && Some(key) == expected_key + } +} diff --git a/src/client/mod.rs b/src/client/mod.rs index eb97178..3b24017 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -3,5 +3,7 @@ mod ui; mod colors; mod components; mod terminal; +mod config; pub use ui::run_ui; +pub use config::Config; diff --git a/src/client/terminal.rs b/src/client/terminal.rs index 391c654..d1a8bf5 100644 --- a/src/client/terminal.rs +++ b/src/client/terminal.rs @@ -1,5 +1,5 @@ // src/client/terminal.rs -use crossterm::event::{self, Event}; +use crossterm::event::{self, Event, KeyCode, KeyModifiers}; use crossterm::{ execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, @@ -8,6 +8,7 @@ use ratatui::{backend::CrosstermBackend, Terminal}; use std::io::{self, stdout}; use tonic::transport::Channel; use crate::proto::multieko2::adresar_client::AdresarClient; +use crate::client::config::Config; use crate::proto::multieko2::AdresarRequest; pub struct AppTerminal { @@ -49,12 +50,12 @@ impl AppTerminal { pub async fn handle_command( &mut self, - command_input: &str, + action: &str, is_saved: &mut bool, - form_data: &AdresarRequest, // Pass form data here + form_data: &AdresarRequest, ) -> Result<(bool, String), Box> { - match command_input { - "w" => { + match action { + "save" => { // Send data to the server let request = tonic::Request::new(form_data.clone()); let response = self.grpc_client.create_adresar(request).await?; @@ -62,7 +63,7 @@ impl AppTerminal { *is_saved = true; Ok((false, format!("State saved. Response: {:?}", response))) } - "q" => { + "quit" => { if *is_saved { self.cleanup()?; Ok((true, "Exiting.".to_string())) @@ -70,16 +71,16 @@ impl AppTerminal { Ok((false, "No changes saved. Use :q! to force quit.".to_string())) } } - "q!" => { + "force_quit" => { self.cleanup()?; Ok((true, "Force exiting without saving.".to_string())) } - "wq" => { + "save_and_quit" => { *is_saved = true; self.cleanup()?; Ok((true, "State saved. Exiting.".to_string())) } - _ => Ok((false, format!("Command not recognized: {}", command_input))), + _ => Ok((false, format!("Action not recognized: {}", action))), } } } diff --git a/src/client/ui.rs b/src/client/ui.rs index 5cb6a2b..0f8c362 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -3,11 +3,13 @@ use crossterm::event::{Event, KeyCode, KeyModifiers}; use crate::client::terminal::AppTerminal; use crate::client::components::{render_command_line, render_form, render_preview_card, render_status_line}; use crate::client::colors::Theme; +use crate::client::config::Config; use ratatui::layout::{Constraint, Direction, Layout, Rect}; use std::env; use crate::proto::multieko2::AdresarRequest; pub async fn run_ui() -> Result<(), Box> { + let config = Config::load()?; let mut app_terminal = AppTerminal::new().await?; let mut command_mode = false; let mut command_input = String::new(); @@ -118,8 +120,10 @@ pub async fn run_ui() -> Result<(), Box> { fax: fax.clone(), }; - // Pass form data to handle_command - let (should_exit, message) = app_terminal.handle_command(&command_input, &mut is_saved, &form_data).await?; + // Pass form data to handle_command (remove &config) + let (should_exit, message) = app_terminal + .handle_command(&command_input, &mut is_saved, &form_data) + .await?; command_message = message; if should_exit { return Ok(()); @@ -139,64 +143,94 @@ pub async fn run_ui() -> Result<(), Box> { _ => {} } } else { - match key.code { - KeyCode::Char(':') => { - command_mode = true; - command_input.clear(); - command_message.clear(); + // Check for keybindings + if let Some(action) = config.get_action_for_key(key.code, key.modifiers) { + let form_data = AdresarRequest { + firma: firma.clone(), + kz: kz.clone(), + drc: drc.clone(), + ulica: ulica.clone(), + psc: psc.clone(), + mesto: mesto.clone(), + stat: stat.clone(), + banka: banka.clone(), + ucet: ucet.clone(), + skladm: skladm.clone(), + ico: ico.clone(), + kontakt: kontakt.clone(), + telefon: telefon.clone(), + skladu: skladu.clone(), + fax: fax.clone(), + }; + + // Pass form data to handle_command (remove &config) + let (should_exit, message) = app_terminal + .handle_command(action, &mut is_saved, &form_data) + .await?; + command_message = message; + if should_exit { + return Ok(()); } - KeyCode::Tab => { - if key.modifiers.contains(KeyModifiers::SHIFT) { - current_field = current_field.saturating_sub(1); - } else { - current_field = (current_field + 1) % fields.len(); + } else { + match key.code { + KeyCode::Char(':') => { + command_mode = true; + command_input.clear(); + command_message.clear(); } - } - KeyCode::BackTab => current_field = current_field.saturating_sub(1), - KeyCode::Down => current_field = (current_field + 1) % fields.len(), - KeyCode::Up => current_field = current_field.saturating_sub(1), - KeyCode::Enter => current_field = (current_field + 1) % fields.len(), - KeyCode::Char(c) => { - match current_field { - 0 => firma.push(c), - 1 => kz.push(c), - 2 => drc.push(c), - 3 => ulica.push(c), - 4 => psc.push(c), - 5 => mesto.push(c), - 6 => stat.push(c), - 7 => banka.push(c), - 8 => ucet.push(c), - 9 => skladm.push(c), - 10 => ico.push(c), - 11 => kontakt.push(c), - 12 => telefon.push(c), - 13 => skladu.push(c), - 14 => fax.push(c), - _ => (), + KeyCode::Tab => { + if key.modifiers.contains(KeyModifiers::SHIFT) { + current_field = current_field.saturating_sub(1); + } else { + current_field = (current_field + 1) % fields.len(); + } } + KeyCode::BackTab => current_field = current_field.saturating_sub(1), + KeyCode::Down => current_field = (current_field + 1) % fields.len(), + KeyCode::Up => current_field = current_field.saturating_sub(1), + KeyCode::Enter => current_field = (current_field + 1) % fields.len(), + KeyCode::Char(c) => { + match current_field { + 0 => firma.push(c), + 1 => kz.push(c), + 2 => drc.push(c), + 3 => ulica.push(c), + 4 => psc.push(c), + 5 => mesto.push(c), + 6 => stat.push(c), + 7 => banka.push(c), + 8 => ucet.push(c), + 9 => skladm.push(c), + 10 => ico.push(c), + 11 => kontakt.push(c), + 12 => telefon.push(c), + 13 => skladu.push(c), + 14 => fax.push(c), + _ => (), + } + } + KeyCode::Backspace => { + match current_field { + 0 => firma.pop(), + 1 => kz.pop(), + 2 => drc.pop(), + 3 => ulica.pop(), + 4 => psc.pop(), + 5 => mesto.pop(), + 6 => stat.pop(), + 7 => banka.pop(), + 8 => ucet.pop(), + 9 => skladm.pop(), + 10 => ico.pop(), + 11 => kontakt.pop(), + 12 => telefon.pop(), + 13 => skladu.pop(), + 14 => fax.pop(), + _ => None, + }; + } + _ => {} } - KeyCode::Backspace => { - match current_field { - 0 => firma.pop(), - 1 => kz.pop(), - 2 => drc.pop(), - 3 => ulica.pop(), - 4 => psc.pop(), - 5 => mesto.pop(), - 6 => stat.pop(), - 7 => banka.pop(), - 8 => ucet.pop(), - 9 => skladm.pop(), - 10 => ico.pop(), - 11 => kontakt.pop(), - 12 => telefon.pop(), - 13 => skladu.pop(), - 14 => fax.pop(), - _ => None, - }; - } - _ => {} } } }