From 3d0359e2c06c7c640f39586d8fc45d822648154e Mon Sep 17 00:00:00 2001 From: HeavyPunk Date: Tue, 25 Aug 2026 00:30:21 +0500 Subject: [PATCH 1/5] feat: initial for hotkey manager --- src/hotkey_manager.rs | 203 ++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 + 2 files changed, 205 insertions(+) create mode 100644 src/hotkey_manager.rs diff --git a/src/hotkey_manager.rs b/src/hotkey_manager.rs new file mode 100644 index 0000000..3e4f256 --- /dev/null +++ b/src/hotkey_manager.rs @@ -0,0 +1,203 @@ +#[derive(Debug, PartialEq)] +pub enum HotKeyError { + Collision +} + +pub struct HotKeyManager { + graph: Graph +} + +impl HotKeyManager { + pub fn new() -> Self { + Self { + graph: Graph::new() + } + } + pub fn register(&mut self, combination: &[char]) -> Result<(), HotKeyError> { + self.graph.write(combination) + } +} + + +struct Graph where TKey: PartialEq + Copy { + begin: GraphNode +} + +#[derive(Debug)] +enum GraphNode { + Node(Option, Box>>), + List(TKey), +} + +impl Graph where TKey: PartialEq + Copy { + pub fn new() -> Self { + Self { + begin: GraphNode::Node(None, Box::new(vec![])) + } + } + + pub fn write(&mut self, payload: &[TKey]) -> Result<(), HotKeyError> { + let _ = Graph::::_write(&mut self.begin, payload)?; + return Ok(()) + } + + fn _write(content: &mut GraphNode, payload: &[TKey]) -> Result { + match content { + GraphNode::Node(value, graph_nodes) => { + if *value != None && *value != Some(payload[0]) { + return Ok(false) + } + + let tail = if *value == None { payload } else { &payload[1..] }; + + for gn in graph_nodes.iter_mut() { + if Graph::::_write(gn, tail)? { + return Ok(true); + } + } + if tail.len() > 1 { + let node = GraphNode::Node(Some(tail[0]), Box::new(vec![])); + graph_nodes.push(node); + let node = graph_nodes.last_mut().unwrap(); + return Graph::::_write(node, tail) + } else { + let node = GraphNode::List(tail[0]); + graph_nodes.push(node); + return Ok(true); + } + }, + GraphNode::List(value) => { + if payload.len() > 0 && *value == payload[0] { + return Err(HotKeyError::Collision); + } + return Ok(false) + }, + } + } +} + +#[cfg(test)] +mod tests { + use crate::hotkey_manager::{Graph, GraphNode, HotKeyError}; + + fn assert_graph(expected: &Graph, actual: &Graph) { + _assert_node(&expected.begin, &actual.begin); + } + + fn _assert_node(expected: &GraphNode, actual: &GraphNode) { + match (expected, actual) { + (GraphNode::List(exp_key), GraphNode::List(act_key)) => assert_eq!(exp_key, act_key), + (GraphNode::Node(exp_key, exp_nodes), GraphNode::Node(act_key, act_nodes)) => { + assert_eq!(exp_key, act_key); + assert_eq!(exp_nodes.len(), act_nodes.len()); + for i in 0..exp_nodes.len() { + _assert_node(&exp_nodes[i], &act_nodes[i]); + } + } + _ => panic!("nodes are not equal") + } + } + + #[test] + fn should_add_single_node() { + let mut graph = Graph::::new(); + let payload = ['a']; + assert_eq!(graph.write(&payload), Ok(())); + assert_graph(&Graph { + begin: GraphNode::Node(None, Box::new(vec![ + GraphNode::List('a') + ])) + }, &graph); + } + + #[test] + fn should_add_single_sequence() { + let mut graph = Graph::::new(); + let payload = ['a', 'b', 'c']; + assert_eq!(graph.write(&payload), Ok(())); + assert_graph(&Graph { + begin: GraphNode::Node(None, Box::new(vec![ + GraphNode::Node(Some('a'), Box::new(vec![ + GraphNode::Node(Some('b'), Box::new(vec![ + GraphNode::List('c') + ])) + ])) + ])) + }, &graph); + } + + #[test] + fn should_add_sequences() { + let mut graph = Graph::::new(); + let payload1 = ['a', 'b', 'c']; + let payload2 = ['a', 'b', 'v']; + assert_eq!(graph.write(&payload1), Ok(())); + assert_eq!(graph.write(&payload2), Ok(())); + assert_graph(&Graph { + begin: GraphNode::Node(None, Box::new(vec![ + GraphNode::Node(Some('a'), Box::new(vec![ + GraphNode::Node(Some('b'), Box::new(vec![ + GraphNode::List('c'), + GraphNode::List('v'), + ])) + ])) + ])) + }, &graph); + } + + #[test] + fn should_add_different_sequences() { + let mut graph = Graph::::new(); + let payload1 = ['a', 'b', 'c']; + let payload2 = ['q', 'w', 'e']; + assert_eq!(graph.write(&payload1), Ok(())); + assert_eq!(graph.write(&payload2), Ok(())); + assert_graph(&Graph { + begin: GraphNode::Node(None, Box::new(vec![ + GraphNode::Node(Some('a'), Box::new(vec![ + GraphNode::Node(Some('b'), Box::new(vec![ + GraphNode::List('c'), + ])) + ])), + GraphNode::Node(Some('q'), Box::new(vec![ + GraphNode::Node(Some('w'), Box::new(vec![ + GraphNode::List('e') + ])) + ])) + ])) + }, &graph); + } + + #[test] + fn should_fail_on_same_sequences() { + let mut graph = Graph::::new(); + let payload1 = ['a', 'b', 'c']; + let payload2 = ['a', 'b', 'c']; + assert_eq!(graph.write(&payload1), Ok(())); + assert_eq!(graph.write(&payload2), Err(HotKeyError::Collision)); + assert_graph(&Graph { + begin: GraphNode::Node(None, Box::new(vec![ + GraphNode::Node(Some('a'), Box::new(vec![ + GraphNode::Node(Some('b'), Box::new(vec![ + GraphNode::List('c'), + ])) + ])) + ])) + }, &graph); + } + + #[test] + fn should_fail_on_same_substring() { + let mut graph = Graph::::new(); + let payload1 = ['a']; + let payload2 = ['a', 'b']; + assert_eq!(graph.write(&payload1), Ok(())); + assert_eq!(graph.write(&payload2), Err(HotKeyError::Collision)); + assert_graph(&Graph { + begin: GraphNode::Node(None, Box::new(vec![ + GraphNode::List('a') + ])) + }, &graph); + } +} + diff --git a/src/lib.rs b/src/lib.rs index cb9938e..fc993aa 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,7 @@ use chrono::{DateTime, Utc}; +pub mod hotkey_manager; + #[derive(Debug, PartialEq, Clone, Hash, Eq)] pub enum Field { String(Option), From 868638ce5c435f41370b8c0146894ee9a466419b Mon Sep 17 00:00:00 2001 From: HeavyPunk Date: Thu, 27 Aug 2026 03:02:13 +0500 Subject: [PATCH 2/5] feat: add hotkey manager --- src/hotkey_manager.rs | 446 ++++++++++++++++++++++++++++++------------ 1 file changed, 316 insertions(+), 130 deletions(-) diff --git a/src/hotkey_manager.rs b/src/hotkey_manager.rs index 3e4f256..46b6a26 100644 --- a/src/hotkey_manager.rs +++ b/src/hotkey_manager.rs @@ -1,49 +1,82 @@ +use std::marker::PhantomData; + #[derive(Debug, PartialEq)] pub enum HotKeyError { - Collision + Collision, + CallbackError, + CombinationNotFound } -pub struct HotKeyManager { - graph: Graph +type HotKeyCallback = fn(&mut CallbackState) -> Result; + +pub struct HotKeyManager { + graph: Graph>, + comb_buf: Vec, } -impl HotKeyManager { +impl HotKeyManager { pub fn new() -> Self { Self { - graph: Graph::new() + graph: Graph::new(), + comb_buf: vec![], } } - pub fn register(&mut self, combination: &[char]) -> Result<(), HotKeyError> { - self.graph.write(combination) + + pub fn register(&mut self, combination: &[TKey], callback: HotKeyCallback) -> Result<(), HotKeyError> { + self.graph.write(combination, callback) + } + + pub fn push(&mut self, payload: TKey) -> Result<(), HotKeyError> { + self.comb_buf.push(payload); + Ok(()) + } + + pub fn invoke_if_matched(&mut self, state: &mut CallbackState) -> Result { + if !self.graph.is_possible_path(&self.comb_buf) { + return Err(HotKeyError::CombinationNotFound) + } + return self.graph.invoke(&self.comb_buf, state); } } +trait GraphCallback: FnMut(&mut State) -> Result {} + +impl GraphCallback for F +where F: FnMut(&mut CallbackState) -> Result +{ +} -struct Graph where TKey: PartialEq + Copy { - begin: GraphNode +struct Graph> { + begin: GraphNode } #[derive(Debug)] -enum GraphNode { - Node(Option, Box>>), - List(TKey), +enum GraphNode> { + Node( + Option, + Box>>, + PhantomData, + PhantomData, + ), + List(TKey, Callback), } -impl Graph where TKey: PartialEq + Copy { +impl> Graph { pub fn new() -> Self { Self { - begin: GraphNode::Node(None, Box::new(vec![])) + begin: GraphNode::Node(None, Box::new(vec![]), PhantomData, PhantomData) } } - pub fn write(&mut self, payload: &[TKey]) -> Result<(), HotKeyError> { - let _ = Graph::::_write(&mut self.begin, payload)?; + pub fn write(&mut self, payload: &[TKey], callback: Callback) -> Result<(), HotKeyError> { + let mut callback = Some(callback); + let _ = Self::_write(&mut self.begin, payload, &mut callback)?; return Ok(()) } - fn _write(content: &mut GraphNode, payload: &[TKey]) -> Result { + fn _write(content: &mut GraphNode, payload: &[TKey], callback: &mut Option) -> Result { match content { - GraphNode::Node(value, graph_nodes) => { + GraphNode::Node(value, graph_nodes, _, _) => { if *value != None && *value != Some(payload[0]) { return Ok(false) } @@ -51,22 +84,23 @@ impl Graph where TKey: PartialEq + Copy { let tail = if *value == None { payload } else { &payload[1..] }; for gn in graph_nodes.iter_mut() { - if Graph::::_write(gn, tail)? { + if Self::_write(gn, tail, callback)? { return Ok(true); } } if tail.len() > 1 { - let node = GraphNode::Node(Some(tail[0]), Box::new(vec![])); + let node = GraphNode::Node(Some(tail[0]), Box::new(vec![]), PhantomData, PhantomData); graph_nodes.push(node); let node = graph_nodes.last_mut().unwrap(); - return Graph::::_write(node, tail) + return Self::_write(node, tail, callback) } else { - let node = GraphNode::List(tail[0]); + let callback = callback.take().unwrap(); + let node = GraphNode::List(tail[0], callback); graph_nodes.push(node); return Ok(true); } }, - GraphNode::List(value) => { + GraphNode::List(value, _) => { if payload.len() > 0 && *value == payload[0] { return Err(HotKeyError::Collision); } @@ -74,130 +108,282 @@ impl Graph where TKey: PartialEq + Copy { }, } } + + pub fn is_possible_path(&self, payload: &[TKey]) -> bool { + if payload.len() == 0 { + return true; + } + Self::_is_possible_path(&self.begin, payload) + } + + fn _is_possible_path(node: &GraphNode, payload: &[TKey]) -> bool { + match node { + GraphNode::Node(value, graph_nodes, _, _) => { + if let Some(v) = *value { + if v != payload[0] { + return false; + } + } + let tail = if *value == None { payload } else { &payload[1..] }; + + if tail.len() > 0 { + for gn in graph_nodes.iter() { + if Self::_is_possible_path(gn, tail) { + return true; + } + } + return false; + } + return true; + }, + GraphNode::List(value, _) => { + return payload.len() == 1 && *value == payload[0] + }, + } + } + + pub fn invoke(&mut self, payload: &[TKey], state: &mut CallbackState) -> Result { + if payload.is_empty() { + return Err(HotKeyError::CombinationNotFound) + } + Self::_invoke(&mut self.begin, payload, state) + } + + fn _invoke(node: &mut GraphNode, payload: &[TKey], state: &mut CallbackState) -> Result { + match node { + GraphNode::Node(value, graph_nodes, _, _) => { + if let Some(v) = *value { + if v != payload[0] { + return Err(HotKeyError::CombinationNotFound) + } + } + + let tail = if *value == None { payload } else { &payload[1..] }; + + if tail.len() > 0 { + for gn in graph_nodes.iter_mut() { + let r = Self::_invoke(gn, tail, state); + match r { + Err(HotKeyError::CombinationNotFound) => continue, + _ => return r + }; + } + return Err(HotKeyError::CombinationNotFound) + } + + return Err(HotKeyError::CombinationNotFound) + }, + GraphNode::List(value, clbk) => { + if payload.len() == 1 && *value == payload[0] { + return clbk(state) + } + Err(HotKeyError::CombinationNotFound) + }, + } + } } #[cfg(test)] mod tests { - use crate::hotkey_manager::{Graph, GraphNode, HotKeyError}; + use crate::hotkey_manager::{Graph, GraphCallback, GraphNode, HotKeyError}; - fn assert_graph(expected: &Graph, actual: &Graph) { - _assert_node(&expected.begin, &actual.begin); + type TestCallback = fn(&mut ()) -> Result<(), HotKeyError>; + + fn noop(_: &mut ()) -> Result<(), HotKeyError> { + Ok(()) } - fn _assert_node(expected: &GraphNode, actual: &GraphNode) { - match (expected, actual) { - (GraphNode::List(exp_key), GraphNode::List(act_key)) => assert_eq!(exp_key, act_key), - (GraphNode::Node(exp_key, exp_nodes), GraphNode::Node(act_key, act_nodes)) => { - assert_eq!(exp_key, act_key); - assert_eq!(exp_nodes.len(), act_nodes.len()); - for i in 0..exp_nodes.len() { - _assert_node(&exp_nodes[i], &act_nodes[i]); + mod graph { + use std::marker::PhantomData; + + use crate::hotkey_manager::{Graph, GraphCallback, GraphNode, HotKeyError}; + use super::{TestCallback, noop}; + + fn assert_graph(expected: &Graph, actual: &Graph) { + _assert_node(&expected.begin, &actual.begin); + } + + fn _assert_node(expected: &GraphNode, actual: &GraphNode) { + match (expected, actual) { + (GraphNode::List(exp_key, _), GraphNode::List(act_key, _)) => assert_eq!(exp_key, act_key), + (GraphNode::Node(exp_key, exp_nodes, _, _), GraphNode::Node(act_key, act_nodes, _, _)) => { + assert_eq!(exp_key, act_key); + assert_eq!(exp_nodes.len(), act_nodes.len()); + for i in 0..exp_nodes.len() { + _assert_node(&exp_nodes[i], &act_nodes[i]); + } } + _ => panic!("nodes are not equal") } - _ => panic!("nodes are not equal") } - } - #[test] - fn should_add_single_node() { - let mut graph = Graph::::new(); - let payload = ['a']; - assert_eq!(graph.write(&payload), Ok(())); - assert_graph(&Graph { - begin: GraphNode::Node(None, Box::new(vec![ - GraphNode::List('a') - ])) - }, &graph); - } + #[test] + fn should_add_single_node() { + let mut graph = Graph::::new(); + let payload = ['a']; + assert_eq!(graph.write(&payload, noop), Ok(())); + assert_graph(&Graph { + begin: GraphNode::Node(None, Box::new(vec![ + GraphNode::List('a', noop) + ]), PhantomData, PhantomData) + }, &graph); + } - #[test] - fn should_add_single_sequence() { - let mut graph = Graph::::new(); - let payload = ['a', 'b', 'c']; - assert_eq!(graph.write(&payload), Ok(())); - assert_graph(&Graph { - begin: GraphNode::Node(None, Box::new(vec![ - GraphNode::Node(Some('a'), Box::new(vec![ - GraphNode::Node(Some('b'), Box::new(vec![ - GraphNode::List('c') - ])) - ])) - ])) - }, &graph); - } + #[test] + fn should_add_single_sequence() { + let mut graph = Graph::::new(); + let payload = ['a', 'b', 'c']; + assert_eq!(graph.write(&payload, noop), Ok(())); + assert_graph(&Graph { + begin: GraphNode::Node(None, Box::new(vec![ + GraphNode::Node(Some('a'), Box::new(vec![ + GraphNode::Node(Some('b'), Box::new(vec![ + GraphNode::List('c', noop) + ]), PhantomData, PhantomData) + ]), PhantomData, PhantomData) + ]), PhantomData, PhantomData) + }, &graph); + } - #[test] - fn should_add_sequences() { - let mut graph = Graph::::new(); - let payload1 = ['a', 'b', 'c']; - let payload2 = ['a', 'b', 'v']; - assert_eq!(graph.write(&payload1), Ok(())); - assert_eq!(graph.write(&payload2), Ok(())); - assert_graph(&Graph { - begin: GraphNode::Node(None, Box::new(vec![ - GraphNode::Node(Some('a'), Box::new(vec![ - GraphNode::Node(Some('b'), Box::new(vec![ - GraphNode::List('c'), - GraphNode::List('v'), - ])) - ])) - ])) - }, &graph); - } + #[test] + fn should_add_sequences() { + let mut graph = Graph::::new(); + let payload1 = ['a', 'b', 'c']; + let payload2 = ['a', 'b', 'v']; + assert_eq!(graph.write(&payload1, noop), Ok(())); + assert_eq!(graph.write(&payload2, noop), Ok(())); + assert_graph(&Graph { + begin: GraphNode::Node(None, Box::new(vec![ + GraphNode::Node(Some('a'), Box::new(vec![ + GraphNode::Node(Some('b'), Box::new(vec![ + GraphNode::List('c', noop), + GraphNode::List('v', noop), + ]), PhantomData, PhantomData) + ]), PhantomData, PhantomData) + ]), PhantomData, PhantomData) + }, &graph); + } - #[test] - fn should_add_different_sequences() { - let mut graph = Graph::::new(); - let payload1 = ['a', 'b', 'c']; - let payload2 = ['q', 'w', 'e']; - assert_eq!(graph.write(&payload1), Ok(())); - assert_eq!(graph.write(&payload2), Ok(())); - assert_graph(&Graph { - begin: GraphNode::Node(None, Box::new(vec![ - GraphNode::Node(Some('a'), Box::new(vec![ - GraphNode::Node(Some('b'), Box::new(vec![ - GraphNode::List('c'), - ])) - ])), - GraphNode::Node(Some('q'), Box::new(vec![ - GraphNode::Node(Some('w'), Box::new(vec![ - GraphNode::List('e') - ])) - ])) - ])) - }, &graph); - } + #[test] + fn should_add_different_sequences() { + let mut graph = Graph::::new(); + let payload1 = ['a', 'b', 'c']; + let payload2 = ['q', 'w', 'e']; + assert_eq!(graph.write(&payload1, noop), Ok(())); + assert_eq!(graph.write(&payload2, noop), Ok(())); + assert_graph(&Graph { + begin: GraphNode::Node(None, Box::new(vec![ + GraphNode::Node(Some('a'), Box::new(vec![ + GraphNode::Node(Some('b'), Box::new(vec![ + GraphNode::List('c', noop), + ]), PhantomData, PhantomData) + ]), PhantomData, PhantomData), + GraphNode::Node(Some('q'), Box::new(vec![ + GraphNode::Node(Some('w'), Box::new(vec![ + GraphNode::List('e', noop) + ]), PhantomData, PhantomData) + ]), PhantomData, PhantomData) + ]), PhantomData, PhantomData) + }, &graph); + } - #[test] - fn should_fail_on_same_sequences() { - let mut graph = Graph::::new(); - let payload1 = ['a', 'b', 'c']; - let payload2 = ['a', 'b', 'c']; - assert_eq!(graph.write(&payload1), Ok(())); - assert_eq!(graph.write(&payload2), Err(HotKeyError::Collision)); - assert_graph(&Graph { - begin: GraphNode::Node(None, Box::new(vec![ - GraphNode::Node(Some('a'), Box::new(vec![ - GraphNode::Node(Some('b'), Box::new(vec![ - GraphNode::List('c'), - ])) - ])) - ])) - }, &graph); + #[test] + fn should_fail_on_same_sequences() { + let mut graph = Graph::::new(); + let payload1 = ['a', 'b', 'c']; + let payload2 = ['a', 'b', 'c']; + assert_eq!(graph.write(&payload1, noop), Ok(())); + assert_eq!(graph.write(&payload2, noop), Err(HotKeyError::Collision)); + assert_graph(&Graph { + begin: GraphNode::Node(None, Box::new(vec![ + GraphNode::Node(Some('a'), Box::new(vec![ + GraphNode::Node(Some('b'), Box::new(vec![ + GraphNode::List('c', noop), + ]), PhantomData, PhantomData) + ]), PhantomData, PhantomData) + ]), PhantomData, PhantomData) + }, &graph); + } + + #[test] + fn should_fail_on_same_substring() { + let mut graph = Graph::::new(); + let payload1 = ['a']; + let payload2 = ['a', 'b']; + assert_eq!(graph.write(&payload1, noop), Ok(())); + assert_eq!(graph.write(&payload2, noop), Err(HotKeyError::Collision)); + assert_graph(&Graph { + begin: GraphNode::Node(None, Box::new(vec![ + GraphNode::List('a', noop) + ]), PhantomData, PhantomData) + }, &graph); + } + + #[test] + fn should_find_path() { + let mut graph = Graph::::new(); + let payload = ['a']; + assert_eq!(graph.write(&payload, noop), Ok(())); + assert!(graph.is_possible_path(&payload)); + } + + #[test] + fn should_find_path_on_one_sequence() { + let mut graph = Graph::::new(); + let payload = ['a', 'b', 'c']; + assert_eq!(graph.write(&payload, noop), Ok(())); + assert!(graph.is_possible_path(&payload)); + } + + #[test] + fn should_find_path_on_one_subsequence() { + let mut graph = Graph::::new(); + let payload = ['a', 'b', 'c']; + assert_eq!(graph.write(&payload, noop), Ok(())); + assert!(graph.is_possible_path(&payload[..2])); + } + + #[test] + fn should_find_path_on_several_subsequences() { + let mut graph = Graph::::new(); + let payload1 = ['a', 'b', 'c']; + let payload2 = ['a', 'g', 'h', 'c']; + assert_eq!(graph.write(&payload1, noop), Ok(())); + assert_eq!(graph.write(&payload2, noop), Ok(())); + assert!(graph.is_possible_path(&payload1[..1])); + assert!(graph.is_possible_path(&payload1[..2])); + assert!(graph.is_possible_path(&payload2[..3])); + } } - #[test] - fn should_fail_on_same_substring() { - let mut graph = Graph::::new(); - let payload1 = ['a']; - let payload2 = ['a', 'b']; - assert_eq!(graph.write(&payload1), Ok(())); - assert_eq!(graph.write(&payload2), Err(HotKeyError::Collision)); - assert_graph(&Graph { - begin: GraphNode::Node(None, Box::new(vec![ - GraphNode::List('a') - ])) - }, &graph); + mod hotkey_manager { + use crate::hotkey_manager::{HotKeyError, HotKeyManager}; + + #[test] + fn should_invoke() { + let mut hk_manager = HotKeyManager::new(); + assert_eq!( + hk_manager.register(&['a'], |_: &mut ()| { + Ok(Some(true)) + }), + Ok(()) + ); + assert_eq!(hk_manager.push('a'), Ok(())); + assert_eq!(hk_manager.invoke_if_matched(&mut ()), Ok(Some(true))) + } + + #[test] + fn should_throw_err_on_invoke() { + let mut hk_manager = HotKeyManager::new(); + assert_eq!( + hk_manager.register(&['a'], |_: &mut ()| { + Ok(Some(true)) + }), + Ok(()) + ); + assert_eq!(hk_manager.push('b'), Ok(())); + assert_eq!(hk_manager.invoke_if_matched(&mut ()), Err(HotKeyError::CombinationNotFound)) + } } } From 0c30bc2d351943e745b8724e46c319b0a5666494 Mon Sep 17 00:00:00 2001 From: HeavyPunk Date: Sat, 3 Oct 2026 15:44:40 +0500 Subject: [PATCH 3/5] feat: add hotkey hook --- Cargo.lock | 1 + Cargo.toml | 1 + src/hotkey_manager.rs | 407 ++++++++++++++++++++++--------- src/ui4/components/table_view.rs | 29 ++- src/ui4/connections_list.rs | 30 ++- src/ui4/control/mod.rs | 86 +++++++ src/ui4/mod.rs | 1 + 7 files changed, 438 insertions(+), 117 deletions(-) create mode 100644 src/ui4/control/mod.rs diff --git a/Cargo.lock b/Cargo.lock index f917c2f..4764822 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -324,6 +324,7 @@ dependencies = [ "async-trait", "chrono", "clap", + "futures", "iocraft", "prost", "prost-build", diff --git a/Cargo.toml b/Cargo.toml index c0ad714..1086aa0 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ prost-types = "0.14" anyhow = "1" async-trait = "0.1" tokio = { version = "1.47", features = ["full"] } +futures = "0.3" #db clients redis = { version = "0.31" } diff --git a/src/hotkey_manager.rs b/src/hotkey_manager.rs index 46b6a26..558cd89 100644 --- a/src/hotkey_manager.rs +++ b/src/hotkey_manager.rs @@ -1,20 +1,35 @@ -use std::marker::PhantomData; +use std::{fmt::Debug, marker::PhantomData}; #[derive(Debug, PartialEq)] pub enum HotKeyError { Collision, CallbackError, - CombinationNotFound + CombinationNotFound, } -type HotKeyCallback = fn(&mut CallbackState) -> Result; +pub trait HotKeyCallback: + FnMut(&mut CallbackState) -> Result + Send +{ +} + +impl HotKeyCallback for F where + F: FnMut(&mut CallbackState) -> Result + Send +{ +} pub struct HotKeyManager { - graph: Graph>, + graph: Graph< + TKey, + CallbackState, + CallbackResult, + Box>, + >, comb_buf: Vec, } -impl HotKeyManager { +impl + HotKeyManager +{ pub fn new() -> Self { Self { graph: Graph::new(), @@ -22,36 +37,70 @@ impl HotKeyManager) -> Result<(), HotKeyError> { - self.graph.write(combination, callback) + pub fn clear_buffer(&mut self) { + self.comb_buf.clear(); + } + + pub fn register( + &mut self, + combination: &[TKey], + callback: impl HotKeyCallback + 'static, + ) -> Result<(), HotKeyError> { + self.graph.write(combination, Box::new(callback)) } pub fn push(&mut self, payload: TKey) -> Result<(), HotKeyError> { self.comb_buf.push(payload); + if !self.graph.is_possible_path(&self.comb_buf) { + self.clear_buffer(); + } Ok(()) } - pub fn invoke_if_matched(&mut self, state: &mut CallbackState) -> Result { - if !self.graph.is_possible_path(&self.comb_buf) { - return Err(HotKeyError::CombinationNotFound) - } - return self.graph.invoke(&self.comb_buf, state); + pub fn invoke_if_matched( + &mut self, + state: &mut CallbackState, + ) -> Result { + let result = if !self.graph.is_possible_path(&self.comb_buf) { + Err(HotKeyError::CombinationNotFound) + } else { + let r = self.graph.invoke(&self.comb_buf, state); + match r { + Err(HotKeyError::CombinationNotFound) => {} + _ => { + self.comb_buf.clear(); + } + } + r + }; + + return result; } } trait GraphCallback: FnMut(&mut State) -> Result {} -impl GraphCallback for F -where F: FnMut(&mut CallbackState) -> Result +impl GraphCallback for F where + F: FnMut(&mut CallbackState) -> Result { } -struct Graph> { - begin: GraphNode +struct Graph< + TKey: PartialEq + Copy, + CallbackState, + CallbackResult, + Callback: GraphCallback, +> { + begin: GraphNode, } #[derive(Debug)] -enum GraphNode> { +enum GraphNode< + TKey, + CallbackState, + CallbackResult, + Callback: GraphCallback, +> { Node( Option, Box>>, @@ -61,51 +110,66 @@ enum GraphNode> Graph { +impl< + TKey: PartialEq + Copy, + CallbackState, + CallbackResult, + Callback: GraphCallback, + > Graph +{ pub fn new() -> Self { Self { - begin: GraphNode::Node(None, Box::new(vec![]), PhantomData, PhantomData) + begin: GraphNode::Node(None, Box::new(vec![]), PhantomData, PhantomData), } } pub fn write(&mut self, payload: &[TKey], callback: Callback) -> Result<(), HotKeyError> { let mut callback = Some(callback); let _ = Self::_write(&mut self.begin, payload, &mut callback)?; - return Ok(()) + return Ok(()); } - fn _write(content: &mut GraphNode, payload: &[TKey], callback: &mut Option) -> Result { + fn _write( + content: &mut GraphNode, + payload: &[TKey], + callback: &mut Option, + ) -> Result { match content { GraphNode::Node(value, graph_nodes, _, _) => { if *value != None && *value != Some(payload[0]) { - return Ok(false) + return Ok(false); } - let tail = if *value == None { payload } else { &payload[1..] }; - + let tail = if *value == None { + payload + } else { + &payload[1..] + }; + for gn in graph_nodes.iter_mut() { if Self::_write(gn, tail, callback)? { return Ok(true); } } if tail.len() > 1 { - let node = GraphNode::Node(Some(tail[0]), Box::new(vec![]), PhantomData, PhantomData); + let node = + GraphNode::Node(Some(tail[0]), Box::new(vec![]), PhantomData, PhantomData); graph_nodes.push(node); let node = graph_nodes.last_mut().unwrap(); - return Self::_write(node, tail, callback) + return Self::_write(node, tail, callback); } else { let callback = callback.take().unwrap(); let node = GraphNode::List(tail[0], callback); graph_nodes.push(node); return Ok(true); } - }, + } GraphNode::List(value, _) => { if payload.len() > 0 && *value == payload[0] { return Err(HotKeyError::Collision); } - return Ok(false) - }, + return Ok(false); + } } } @@ -116,7 +180,10 @@ impl, payload: &[TKey]) -> bool { + fn _is_possible_path( + node: &GraphNode, + payload: &[TKey], + ) -> bool { match node { GraphNode::Node(value, graph_nodes, _, _) => { if let Some(v) = *value { @@ -124,7 +191,11 @@ impl 0 { for gn in graph_nodes.iter() { @@ -135,50 +206,60 @@ impl { - return payload.len() == 1 && *value == payload[0] - }, + } + GraphNode::List(value, _) => return payload.len() == 1 && *value == payload[0], } } - pub fn invoke(&mut self, payload: &[TKey], state: &mut CallbackState) -> Result { + pub fn invoke( + &mut self, + payload: &[TKey], + state: &mut CallbackState, + ) -> Result { if payload.is_empty() { - return Err(HotKeyError::CombinationNotFound) + return Err(HotKeyError::CombinationNotFound); } Self::_invoke(&mut self.begin, payload, state) } - fn _invoke(node: &mut GraphNode, payload: &[TKey], state: &mut CallbackState) -> Result { + fn _invoke( + node: &mut GraphNode, + payload: &[TKey], + state: &mut CallbackState, + ) -> Result { match node { GraphNode::Node(value, graph_nodes, _, _) => { if let Some(v) = *value { if v != payload[0] { - return Err(HotKeyError::CombinationNotFound) + return Err(HotKeyError::CombinationNotFound); } } - let tail = if *value == None { payload } else { &payload[1..] }; + let tail = if *value == None { + payload + } else { + &payload[1..] + }; if tail.len() > 0 { for gn in graph_nodes.iter_mut() { let r = Self::_invoke(gn, tail, state); match r { Err(HotKeyError::CombinationNotFound) => continue, - _ => return r + _ => return r, }; } - return Err(HotKeyError::CombinationNotFound) + return Err(HotKeyError::CombinationNotFound); } - return Err(HotKeyError::CombinationNotFound) - }, + return Err(HotKeyError::CombinationNotFound); + } GraphNode::List(value, clbk) => { if payload.len() == 1 && *value == payload[0] { - return clbk(state) + return clbk(state); } Err(HotKeyError::CombinationNotFound) - }, + } } } } @@ -196,24 +277,35 @@ mod tests { mod graph { use std::marker::PhantomData; + use super::{noop, TestCallback}; use crate::hotkey_manager::{Graph, GraphCallback, GraphNode, HotKeyError}; - use super::{TestCallback, noop}; - fn assert_graph(expected: &Graph, actual: &Graph) { + fn assert_graph( + expected: &Graph, + actual: &Graph, + ) { _assert_node(&expected.begin, &actual.begin); } - fn _assert_node(expected: &GraphNode, actual: &GraphNode) { + fn _assert_node( + expected: &GraphNode, + actual: &GraphNode, + ) { match (expected, actual) { - (GraphNode::List(exp_key, _), GraphNode::List(act_key, _)) => assert_eq!(exp_key, act_key), - (GraphNode::Node(exp_key, exp_nodes, _, _), GraphNode::Node(act_key, act_nodes, _, _)) => { + (GraphNode::List(exp_key, _), GraphNode::List(act_key, _)) => { + assert_eq!(exp_key, act_key) + } + ( + GraphNode::Node(exp_key, exp_nodes, _, _), + GraphNode::Node(act_key, act_nodes, _, _), + ) => { assert_eq!(exp_key, act_key); assert_eq!(exp_nodes.len(), act_nodes.len()); for i in 0..exp_nodes.len() { _assert_node(&exp_nodes[i], &act_nodes[i]); } } - _ => panic!("nodes are not equal") + _ => panic!("nodes are not equal"), } } @@ -222,11 +314,40 @@ mod tests { let mut graph = Graph::::new(); let payload = ['a']; assert_eq!(graph.write(&payload, noop), Ok(())); - assert_graph(&Graph { - begin: GraphNode::Node(None, Box::new(vec![ - GraphNode::List('a', noop) - ]), PhantomData, PhantomData) - }, &graph); + assert_graph( + &Graph { + begin: GraphNode::Node( + None, + Box::new(vec![GraphNode::List('a', noop)]), + PhantomData, + PhantomData, + ), + }, + &graph, + ); + } + + #[test] + fn should_add_single_node_on_double_key() { + let mut graph = Graph::::new(); + let payload = ['a', 'a']; + assert_eq!(graph.write(&payload, noop), Ok(())); + assert_graph( + &Graph { + begin: GraphNode::Node( + None, + Box::new(vec![GraphNode::Node( + Some('a'), + Box::new(vec![GraphNode::List('a', noop)]), + PhantomData, + PhantomData, + )]), + PhantomData, + PhantomData, + ), + }, + &graph, + ); } #[test] @@ -234,15 +355,27 @@ mod tests { let mut graph = Graph::::new(); let payload = ['a', 'b', 'c']; assert_eq!(graph.write(&payload, noop), Ok(())); - assert_graph(&Graph { - begin: GraphNode::Node(None, Box::new(vec![ - GraphNode::Node(Some('a'), Box::new(vec![ - GraphNode::Node(Some('b'), Box::new(vec![ - GraphNode::List('c', noop) - ]), PhantomData, PhantomData) - ]), PhantomData, PhantomData) - ]), PhantomData, PhantomData) - }, &graph); + assert_graph( + &Graph { + begin: GraphNode::Node( + None, + Box::new(vec![GraphNode::Node( + Some('a'), + Box::new(vec![GraphNode::Node( + Some('b'), + Box::new(vec![GraphNode::List('c', noop)]), + PhantomData, + PhantomData, + )]), + PhantomData, + PhantomData, + )]), + PhantomData, + PhantomData, + ), + }, + &graph, + ); } #[test] @@ -252,16 +385,30 @@ mod tests { let payload2 = ['a', 'b', 'v']; assert_eq!(graph.write(&payload1, noop), Ok(())); assert_eq!(graph.write(&payload2, noop), Ok(())); - assert_graph(&Graph { - begin: GraphNode::Node(None, Box::new(vec![ - GraphNode::Node(Some('a'), Box::new(vec![ - GraphNode::Node(Some('b'), Box::new(vec![ - GraphNode::List('c', noop), - GraphNode::List('v', noop), - ]), PhantomData, PhantomData) - ]), PhantomData, PhantomData) - ]), PhantomData, PhantomData) - }, &graph); + assert_graph( + &Graph { + begin: GraphNode::Node( + None, + Box::new(vec![GraphNode::Node( + Some('a'), + Box::new(vec![GraphNode::Node( + Some('b'), + Box::new(vec![ + GraphNode::List('c', noop), + GraphNode::List('v', noop), + ]), + PhantomData, + PhantomData, + )]), + PhantomData, + PhantomData, + )]), + PhantomData, + PhantomData, + ), + }, + &graph, + ); } #[test] @@ -271,20 +418,40 @@ mod tests { let payload2 = ['q', 'w', 'e']; assert_eq!(graph.write(&payload1, noop), Ok(())); assert_eq!(graph.write(&payload2, noop), Ok(())); - assert_graph(&Graph { - begin: GraphNode::Node(None, Box::new(vec![ - GraphNode::Node(Some('a'), Box::new(vec![ - GraphNode::Node(Some('b'), Box::new(vec![ - GraphNode::List('c', noop), - ]), PhantomData, PhantomData) - ]), PhantomData, PhantomData), - GraphNode::Node(Some('q'), Box::new(vec![ - GraphNode::Node(Some('w'), Box::new(vec![ - GraphNode::List('e', noop) - ]), PhantomData, PhantomData) - ]), PhantomData, PhantomData) - ]), PhantomData, PhantomData) - }, &graph); + assert_graph( + &Graph { + begin: GraphNode::Node( + None, + Box::new(vec![ + GraphNode::Node( + Some('a'), + Box::new(vec![GraphNode::Node( + Some('b'), + Box::new(vec![GraphNode::List('c', noop)]), + PhantomData, + PhantomData, + )]), + PhantomData, + PhantomData, + ), + GraphNode::Node( + Some('q'), + Box::new(vec![GraphNode::Node( + Some('w'), + Box::new(vec![GraphNode::List('e', noop)]), + PhantomData, + PhantomData, + )]), + PhantomData, + PhantomData, + ), + ]), + PhantomData, + PhantomData, + ), + }, + &graph, + ); } #[test] @@ -294,15 +461,27 @@ mod tests { let payload2 = ['a', 'b', 'c']; assert_eq!(graph.write(&payload1, noop), Ok(())); assert_eq!(graph.write(&payload2, noop), Err(HotKeyError::Collision)); - assert_graph(&Graph { - begin: GraphNode::Node(None, Box::new(vec![ - GraphNode::Node(Some('a'), Box::new(vec![ - GraphNode::Node(Some('b'), Box::new(vec![ - GraphNode::List('c', noop), - ]), PhantomData, PhantomData) - ]), PhantomData, PhantomData) - ]), PhantomData, PhantomData) - }, &graph); + assert_graph( + &Graph { + begin: GraphNode::Node( + None, + Box::new(vec![GraphNode::Node( + Some('a'), + Box::new(vec![GraphNode::Node( + Some('b'), + Box::new(vec![GraphNode::List('c', noop)]), + PhantomData, + PhantomData, + )]), + PhantomData, + PhantomData, + )]), + PhantomData, + PhantomData, + ), + }, + &graph, + ); } #[test] @@ -312,11 +491,17 @@ mod tests { let payload2 = ['a', 'b']; assert_eq!(graph.write(&payload1, noop), Ok(())); assert_eq!(graph.write(&payload2, noop), Err(HotKeyError::Collision)); - assert_graph(&Graph { - begin: GraphNode::Node(None, Box::new(vec![ - GraphNode::List('a', noop) - ]), PhantomData, PhantomData) - }, &graph); + assert_graph( + &Graph { + begin: GraphNode::Node( + None, + Box::new(vec![GraphNode::List('a', noop)]), + PhantomData, + PhantomData, + ), + }, + &graph, + ); } #[test] @@ -363,9 +548,7 @@ mod tests { fn should_invoke() { let mut hk_manager = HotKeyManager::new(); assert_eq!( - hk_manager.register(&['a'], |_: &mut ()| { - Ok(Some(true)) - }), + hk_manager.register(&['a'], |_: &mut ()| { Ok(Some(true)) }), Ok(()) ); assert_eq!(hk_manager.push('a'), Ok(())); @@ -376,14 +559,14 @@ mod tests { fn should_throw_err_on_invoke() { let mut hk_manager = HotKeyManager::new(); assert_eq!( - hk_manager.register(&['a'], |_: &mut ()| { - Ok(Some(true)) - }), + hk_manager.register(&['a'], move |_: &mut ()| { Ok(Some(true)) }), Ok(()) ); assert_eq!(hk_manager.push('b'), Ok(())); - assert_eq!(hk_manager.invoke_if_matched(&mut ()), Err(HotKeyError::CombinationNotFound)) + assert_eq!( + hk_manager.invoke_if_matched(&mut ()), + Err(HotKeyError::CombinationNotFound) + ) } } } - diff --git a/src/ui4/components/table_view.rs b/src/ui4/components/table_view.rs index ac1469d..9e68399 100644 --- a/src/ui4/components/table_view.rs +++ b/src/ui4/components/table_view.rs @@ -5,12 +5,12 @@ use iocraft::{ components::{BorderStyle, ScrollView, ScrollViewHandle, Text, View}, element, hooks::{UseRef, UseState, UseTerminalEvents}, - AnyElement, Color, Edges, FlexDirection, Hooks, KeyCode, KeyEvent, KeyEventKind, Props, - TerminalEvent, + AnyElement, Color, Edges, FlexDirection, Hooks, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, + Props, TerminalEvent, }; use tokio::sync::Mutex as TokioMutex; -use crate::core::proto::common::db_field::Field; +use crate::{core::proto::common::db_field::Field, ui4::control::UseHotkeys}; pub type TableKeyHandler = Arc, Vec)) + Send>>>; pub fn table_key_handler(callback: F) -> TableKeyHandler @@ -125,6 +125,29 @@ pub fn TableView(props: &TableViewProps, mut hooks: Hooks) -> impl Into { + fn use_hotkeys( + &mut self, + is_focused: FocusGetter, + registerer: Registerer, + ) where + Registerer: + FnMut(&mut HotKeyManager<(KeyCode, KeyModifiers, KeyEventKind), (), ()>) + Send + 'a, + FocusGetter: Fn() -> bool + Send + Unpin + 'static; +} + +impl<'a> UseHotkeys<'a> for Hooks<'a, '_> { + fn use_hotkeys( + &mut self, + is_focused: FocusGetter, + mut registerer: Registerer, + ) where + Registerer: + FnMut(&mut HotKeyManager<(KeyCode, KeyModifiers, KeyEventKind), (), ()>) + Send + 'a, + FocusGetter: Fn() -> bool + Send + Unpin + 'static, + { + let mut hotkey_manager = HotKeyManager::new(); + registerer(&mut hotkey_manager); + let h = self.use_hook(move || UseHotkeysImpl { + events: None, + hotkey_manager: hotkey_manager, + is_focused: None, + }); + h.is_focused = Some(is_focused); + } +} + +struct UseHotkeysImpl bool> { + is_focused: Option, + hotkey_manager: HotKeyManager<(KeyCode, KeyModifiers, KeyEventKind), (), ()>, + events: Option, +} + +impl bool + Send + Unpin> Hook for UseHotkeysImpl { + fn poll_change( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context, + ) -> std::task::Poll<()> { + while let Some(Poll::Ready(Some(event))) = self + .events + .as_mut() + .map(|events| pin!(events).poll_next(cx)) + { + match event { + iocraft::TerminalEvent::Key(KeyEvent { + code, + kind, + modifiers, + .. + }) => { + if kind == KeyEventKind::Release { + continue; + } + if let Some(is_focused) = &self.is_focused { + if !is_focused() { + self.hotkey_manager.clear_buffer(); + return Poll::Pending; + } + } + let _ = self.hotkey_manager.push((code, modifiers, kind)); + // TODO: log debug if combination doesn't match + let _ = self.hotkey_manager.invoke_if_matched(&mut ()); + } + _ => {} + } + } + Poll::Pending + } + + fn post_component_update(&mut self, updater: &mut iocraft::prelude::ComponentUpdater) { + if self.events.is_none() { + self.events = updater.terminal_events(); + } + } +} diff --git a/src/ui4/mod.rs b/src/ui4/mod.rs index 11668d1..8bcf06b 100644 --- a/src/ui4/mod.rs +++ b/src/ui4/mod.rs @@ -1,6 +1,7 @@ pub mod app_state; pub mod components; pub mod connections_list; +pub mod control; pub mod db_objects; pub mod file_tree; pub mod forms; From 3d70d3a59029d2c9ad406abe3a54d7c3020416a9 Mon Sep 17 00:00:00 2001 From: HeavyPunk Date: Sun, 4 Oct 2026 02:39:40 +0500 Subject: [PATCH 4/5] feat: add key combinations --- src/ui4/components/table_view.rs | 128 ++++++++++++++++------- src/ui4/connections_list.rs | 36 +++++-- src/ui4/control/mod.rs | 14 +-- src/ui4/db_objects.rs | 5 +- src/ui4/forms/add_record_to_object.rs | 5 +- src/ui4/forms/update_record_of_object.rs | 9 +- src/ui4/query_result.rs | 80 +++++++------- 7 files changed, 182 insertions(+), 95 deletions(-) diff --git a/src/ui4/components/table_view.rs b/src/ui4/components/table_view.rs index 9e68399..24efdac 100644 --- a/src/ui4/components/table_view.rs +++ b/src/ui4/components/table_view.rs @@ -12,10 +12,10 @@ use tokio::sync::Mutex as TokioMutex; use crate::{core::proto::common::db_field::Field, ui4::control::UseHotkeys}; -pub type TableKeyHandler = Arc, Vec)) + Send>>>; -pub fn table_key_handler(callback: F) -> TableKeyHandler +pub type TableEventHandler = Arc, Vec) + Send>>>; +pub fn table_event_handler(callback: F) -> TableEventHandler where - F: FnMut(KeyCode, (Vec, Vec)) + Send + 'static, + F: FnMut(Vec, Vec) + Send + 'static, { Arc::new(Mutex::new(Box::new(callback))) } @@ -26,7 +26,9 @@ pub struct TableViewProps { pub rows: Vec>, pub has_focus: bool, pub selected_row: Arc>>, - pub on_key: Option, + pub on_add_row: Option, + pub on_update_row: Option, + pub on_delete_row: Option, } fn field_to_string(field: &Field) -> String { @@ -57,9 +59,16 @@ fn field_to_string(field: &Field) -> String { pub fn TableView(props: &TableViewProps, mut hooks: Hooks) -> impl Into> { let mut cursor = hooks.use_state(|| 0usize); let mut scroll_handle = hooks.use_ref_default::(); + let rows = props.rows.clone(); + let mut rows_ref = hooks.use_ref(|| rows.clone()); + rows_ref.set(rows.clone()); let columns = props.columns.clone(); + let mut columns_ref = hooks.use_ref(|| columns.clone()); + columns_ref.set(columns.clone()); let row_count = rows.len(); + let mut row_count_ref = hooks.use_ref(|| row_count.clone()); + row_count_ref.set(row_count.clone()); if rows.is_empty() { cursor.set(0); @@ -90,44 +99,29 @@ pub fn TableView(props: &TableViewProps, mut hooks: Hooks) -> impl Into 0 => { - cursor.set((cursor.get() + 1).min(row_count - 1)); - } - KeyCode::Up | KeyCode::Char('k') if row_count > 0 => { - cursor.set(cursor.get().saturating_sub(1)); - } - _ => {} - } - }); + let on_add_row = props.on_add_row.clone(); + let on_update_row = props.on_update_row.clone(); + let on_delete_row = props.on_delete_row.clone(); hooks.use_hotkeys( move || has_focus, move |hotkey_manager| { + let _ = hotkey_manager.register( + &[(KeyCode::Char('k'), KeyModifiers::NONE, KeyEventKind::Press)], + move |_: &mut ()| { + cursor.set(cursor.get().saturating_sub(1)); + Ok(()) + }, + ); + let _ = hotkey_manager.register( + &[(KeyCode::Char('j'), KeyModifiers::NONE, KeyEventKind::Press)], + move |_: &mut ()| { + cursor.set((cursor.get() + 1).min(row_count_ref.get() - 1)); + Ok(()) + }, + ); let _ = hotkey_manager.register( &[ (KeyCode::Char('g'), KeyModifiers::NONE, KeyEventKind::Press), @@ -141,10 +135,70 @@ pub fn TableView(props: &TableViewProps, mut hooks: Hooks) -> impl Into impl Into> { let connections = hooks.use_state(Vec::::new); let mut selected_row = hooks.use_state(|| 0); + let mut system = hooks.use_context_mut::(); + let mut should_exit = hooks.use_state(|| false); if let Some(client) = props.connections_client.clone() { let mut connections = connections.clone(); @@ -67,6 +69,10 @@ pub async fn ConnectionsList( }, ); + if should_exit.get() { + system.exit(); + } + let state = props.state.clone(); hooks.use_terminal_events({ move |event| match event { @@ -97,6 +103,9 @@ pub async fn ConnectionsList( guard.selected_connection = Some(conn.unwrap().clone()); guard.selected_page = Page::QueryArea; } + KeyCode::Esc => { + should_exit.set(true); + } _ => {} } } @@ -122,15 +131,30 @@ pub async fn ConnectionsList( } #(connections.read().iter().enumerate().map(|(i, connection)| element! { - // View(background_color: if selected_row.get() == i { Some(Color::Blue) } else { if i % 2 == 0 {None} else {Some(Color::DarkGrey)} }) { View( - background_color: if selected_row.get() == i { Some(Color::Blue) } else { None }, + background_color: if selected_row.get() == i { + Some(Color::Blue) + } else if i % 2 == 0 { + None + } else { + Some(Color::DarkGrey) + } ) { View(width: 50pct, justify_content: JustifyContent::Start, padding_right: 2) { - Text(content: connection.id.clone(), color: if selected_row.get() == i {Some(Color::Black)} else {None}) + Text( + content: connection.id.clone(), + color: if selected_row.get() == i || i % 2 != 0 { + Some(Color::Black) + } else {None} + ) } View(width: 50pct, justify_content: JustifyContent::Center, padding_right: 2) { - Text(content: "TODO".to_string(), color: if selected_row.get() == i {Some(Color::Black)} else {None}) + Text( + content: "TODO".to_string(), + color: if selected_row.get() == i || i % 2 != 0 { + Some(Color::Black) + } else {None} + ) } } })) diff --git a/src/ui4/control/mod.rs b/src/ui4/control/mod.rs index be729ef..f1bd05d 100644 --- a/src/ui4/control/mod.rs +++ b/src/ui4/control/mod.rs @@ -4,25 +4,27 @@ use dbclient::hotkey_manager::HotKeyManager; use futures::Stream; use iocraft::{Hook, Hooks, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, TerminalEvents}; -pub trait UseHotkeys<'a> { +pub trait UseHotkeys { fn use_hotkeys( &mut self, is_focused: FocusGetter, registerer: Registerer, ) where - Registerer: - FnMut(&mut HotKeyManager<(KeyCode, KeyModifiers, KeyEventKind), (), ()>) + Send + 'a, + Registerer: FnMut(&mut HotKeyManager<(KeyCode, KeyModifiers, KeyEventKind), (), ()>) + + Send + + 'static, FocusGetter: Fn() -> bool + Send + Unpin + 'static; } -impl<'a> UseHotkeys<'a> for Hooks<'a, '_> { +impl UseHotkeys for Hooks<'_, '_> { fn use_hotkeys( &mut self, is_focused: FocusGetter, mut registerer: Registerer, ) where - Registerer: - FnMut(&mut HotKeyManager<(KeyCode, KeyModifiers, KeyEventKind), (), ()>) + Send + 'a, + Registerer: FnMut(&mut HotKeyManager<(KeyCode, KeyModifiers, KeyEventKind), (), ()>) + + Send + + 'static, FocusGetter: Fn() -> bool + Send + Unpin + 'static, { let mut hotkey_manager = HotKeyManager::new(); diff --git a/src/ui4/db_objects.rs b/src/ui4/db_objects.rs index abbc3f6..6382820 100644 --- a/src/ui4/db_objects.rs +++ b/src/ui4/db_objects.rs @@ -15,7 +15,7 @@ use crate::{ common::{self, db_object::Specification, postgres_object::Object}, }, ui4::{ - app_state::{AppState, QueryResultCmd, Widget}, + app_state::{AppState, Page, QueryResultCmd, Widget}, file_tree::{FileTree, FileTreeNode}, }, }; @@ -268,6 +268,9 @@ pub fn DbObjects(props: &DbObjectsProps, mut hooks: Hooks) -> impl Into { state.focus_widget = Widget::QueryResult; } + KeyCode::Esc => { + state.selected_page = Page::ConnectionsList; + } _ => {} }; }); diff --git a/src/ui4/forms/add_record_to_object.rs b/src/ui4/forms/add_record_to_object.rs index dc0a816..d5bab87 100644 --- a/src/ui4/forms/add_record_to_object.rs +++ b/src/ui4/forms/add_record_to_object.rs @@ -126,7 +126,10 @@ fn FormTextInput(props: &FormTextInputProps, _hooks: Hooks) -> impl Into impl Into { state.focus_widget = crate::ui4::app_state::Widget::DbObjects; } + KeyCode::Esc => { + state.selected_page = Page::ConnectionsList; + } _ => {} } }); @@ -312,7 +315,8 @@ pub fn QueryResult( #( match postgres_record.record.as_ref() { Some(proto::common::postgres_record::Record::Table(table)) => { - let table_callback_state = props.state.clone(); + let table_callback_state_on_add = props.state.clone(); + let table_callback_state_on_update = props.state.clone(); let cmd_sender = props.cmd_pipe.clone(); element! { TableView( @@ -320,44 +324,38 @@ pub fn QueryResult( rows: table.rows.iter().map(|row| row.values.iter().filter_map(|value| value.field.clone()).collect::>()).collect::>>(), has_focus: tokio::task::block_in_place(|| props.state.blocking_lock().focus_widget == crate::ui4::app_state::Widget::QueryResult), selected_row: selected_row.read().clone(), - on_key: Some(table_key_handler(move |code, (columns, row)| { - match code { - KeyCode::Char('a') => { - let mut state_guard = tokio::task::block_in_place(|| table_callback_state.blocking_lock()); - render_popup.set(Some(Popup::AddRecordToObject(false))); - state_guard.focus_widget = crate::ui4::app_state::Widget::AnyPopup; - - } - KeyCode::Char('d') => { - let cmd = QueryResultCmd::RemoveRecordFromObject(proto::common::DbRecord { - specification: Some(proto::common::db_record::Specification::Postgres(proto::common::PostgresRecord { - record: Some(proto::common::postgres_record::Record::Table(proto::common::PostgresRecordTable { - rows: vec![PostgresRecordTableRow { - columns: columns.iter().map(|c| proto::common::PostgresTableColumn { name: c.clone(), field: None }).collect(), - values: row.iter().map(|r| proto::common::DbField { field: Some(r.clone()) }).collect() - }] - })) - })) - }); - tokio::task::block_in_place(|| cmd_sender.as_ref().and_then(|ch| Some(ch.blocking_send(cmd)))); - }, - KeyCode::Char('i') => { - let mut state_guard = tokio::task::block_in_place(|| table_callback_state.blocking_lock()); - let initial_record = proto::common::DbRecord { - specification: Some(proto::common::db_record::Specification::Postgres(proto::common::PostgresRecord { - record: Some(proto::common::postgres_record::Record::Table(proto::common::PostgresRecordTable { - rows: vec![PostgresRecordTableRow { - columns: columns.iter().map(|c| proto::common::PostgresTableColumn { name: c.clone(), field: None }).collect(), - values: row.iter().map(|r| proto::common::DbField { field: Some(r.clone()) }).collect() - }] - })) - })) - }; - render_popup.set(Some(Popup::UpdateRecordOfObject(initial_record))); - state_guard.focus_widget = crate::ui4::app_state::Widget::AnyPopup; - } - _ => {} - } + on_add_row: Some(table_event_handler(move |_, _| { + let mut state_guard = tokio::task::block_in_place(|| table_callback_state_on_add.blocking_lock()); + render_popup.set(Some(Popup::AddRecordToObject(false))); + state_guard.focus_widget = crate::ui4::app_state::Widget::AnyPopup; + })), + on_update_row: Some(table_event_handler(move |columns, row| { + let mut state_guard = tokio::task::block_in_place(|| table_callback_state_on_update.blocking_lock()); + let initial_record = proto::common::DbRecord { + specification: Some(proto::common::db_record::Specification::Postgres(proto::common::PostgresRecord { + record: Some(proto::common::postgres_record::Record::Table(proto::common::PostgresRecordTable { + rows: vec![PostgresRecordTableRow { + columns: columns.iter().map(|c| proto::common::PostgresTableColumn { name: c.clone(), field: None }).collect(), + values: row.iter().map(|r| proto::common::DbField { field: Some(r.clone()) }).collect() + }] + })) + })) + }; + render_popup.set(Some(Popup::UpdateRecordOfObject(initial_record))); + state_guard.focus_widget = crate::ui4::app_state::Widget::AnyPopup; + })), + on_delete_row: Some(table_event_handler(move |columns, row| { + let cmd = QueryResultCmd::RemoveRecordFromObject(proto::common::DbRecord { + specification: Some(proto::common::db_record::Specification::Postgres(proto::common::PostgresRecord { + record: Some(proto::common::postgres_record::Record::Table(proto::common::PostgresRecordTable { + rows: vec![PostgresRecordTableRow { + columns: columns.iter().map(|c| proto::common::PostgresTableColumn { name: c.clone(), field: None }).collect(), + values: row.iter().map(|r| proto::common::DbField { field: Some(r.clone()) }).collect() + }] + })) + })) + }); + tokio::task::block_in_place(|| cmd_sender.as_ref().and_then(|ch| Some(ch.blocking_send(cmd)))); })) ) } From fec6690e11ac488770f1e461dc4b4b55d3eda8ba Mon Sep 17 00:00:00 2001 From: HeavyPunk Date: Sun, 4 Oct 2026 03:00:05 +0500 Subject: [PATCH 5/5] fix: change version --- .github/workflows/tag.yml | 25 ++++++++++++++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 45 +++++++++++++++++---------------------- 4 files changed, 46 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/tag.yml diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml new file mode 100644 index 0000000..1513e07 --- /dev/null +++ b/.github/workflows/tag.yml @@ -0,0 +1,25 @@ +name: Tag master + +on: + push: + branches: [ "master" ] + +permissions: + contents: write + +jobs: + tag: + runs-on: ubuntu-latest + + steps: + - name: Create tag + env: + GH_TOKEN: ${{ github.token }} + run: | + TAG=$(cargo metadata --format-version=1 --no-deps | jq -r '.packages[0].version') + + gh api \ + --method POST \ + "/repos/${{ github.repository }}/git/refs" \ + -f ref="refs/tags/$TAG" \ + -f sha="${{ github.sha }}" diff --git a/Cargo.lock b/Cargo.lock index 4764822..208ecf9 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -318,7 +318,7 @@ dependencies = [ [[package]] name = "dbclient" -version = "0.4.1" +version = "0.4.2" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 1086aa0..f56aeeb 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dbclient" -version = "0.4.1" +version = "0.4.2" edition = "2021" [dependencies] diff --git a/README.md b/README.md index 94f13a6..564dc91 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,12 @@ A universal Terminal User Interface (TUI) client for database management with Vi ## Features ### Currently Supported -- **Redis** - Read-only operations -- **Read-only mode** - Browse and query your databases safely -- **Executing custom query** - You can execute any query of any type (RW-mode) +- **PostgreSQL support** - Partial integration with Postgres databases +- **Operations** - Read, create, update, and delete functionality for PostgreSQL databases ### Planned Features -- **PostgreSQL support** - Full integration with Postgres databases +- **Redis support** - Full integration with Redis databases - **MySQL support** - Complete MySQL database management -- **Write operations** - Create, update, and delete functionality for all supported databases - **UI improvements** - Better UI interactions (for example: horizontal scrolling) - **UX improvements** - For example: get notification about any error instead of fall to panic @@ -50,30 +48,25 @@ dbclient --config-path config.toml #### Query page - Database objects widget: - `j|k|↑|↓` - Navigate through objects - - `/` - Search - - `n` - Go to next search pattern matching - - `N` - Go to previous search pattern matching + - `` - Open object sub-tree - `` - Get all items in selected object - - `L|→` - Go to query result widget + - `l|→` - Go to query result widget - `` - Quit to main page - Query result widget: - `j|k|↑|↓` - Navigate through records - - `/` - Search - - `n` - Go to next search pattern matching - - `N` - Go to previous search pattern matching - - `q` - Open query input popup - - `H|←` - Go to database objects widget - - `g` - Go to the first record (in future will be replaced with `gg`) + - `h|←` - Go to database objects widget + - `gg` - Go to the first record - `G` - Go to the last record + - `a` - Open popup to add record into database object + - `i` - Open popup to modify record of database object + - `dd` - Remove record of database object - `` - Quit -- Search popup: - - `i` - Activate insert mode - - `` - If in insert mode then activate normal mode else - close popup - - `` - In normal mode, apply search pattern. After this use `` to close popup -- Query popup: - - `i` - Activate insert mode - - `` - If in insert mode then activate normal mode else - close popup - - `` - In normal mode, apply query. After this use `` to close popup +- Popup: + - `j|k|↑|↓` - Navigate through fields + - `i` - Activate insert mode for selected field + - `` in insert mode - Activate normal navigation mode through fields + - `` in normal mode - Close popup + - `` in normal mode - Apply operation ## Configuration @@ -81,9 +74,9 @@ Create a configuration file any directory: ```toml [[connections]] -connection_type = "Redis" +connection_type = "Postgres" name = "local" -connection_string = "redis://localhost:6379" +connection_string = "postgresql://user:password@localhost/postgres?connect_timeout=10" ``` ## Requirements @@ -118,5 +111,5 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ## Acknowledgments -- Built with [tui-realm](https://github.com/veeso/tui-realm) TUI framework +- Built with [iocraft](https://github.com/ccbrown/iocraft) TUI framework - Inspired by various database GUI clients