diff --git a/ratisui-core/src/ssh_tunnel.rs b/ratisui-core/src/ssh_tunnel.rs index 225e610..ea2ef88 100644 --- a/ratisui-core/src/ssh_tunnel.rs +++ b/ratisui-core/src/ssh_tunnel.rs @@ -10,6 +10,7 @@ use tokio::io::AsyncWriteExt; use tokio::net::TcpListener; use tokio::select; +/// The locally opened TcpListener here can be accessed by other processes, which may not be secure. #[derive(Clone, Debug)] pub struct SshTunnel { pub host: String, @@ -20,7 +21,7 @@ pub struct SshTunnel { pub forwarding_port: u16, tx: tokio::sync::watch::Sender, rx: tokio::sync::watch::Receiver, - is_connected: bool, + socket_addr: Option, } impl SshTunnel { @@ -35,11 +36,14 @@ impl SshTunnel { forwarding_port, tx, rx, - is_connected: false, + socket_addr: None, } } pub async fn open(&mut self) -> Result { + if let Some(addr) = self.socket_addr { + return Ok(addr); + } let mut ssh_client = russh::client::connect( Arc::new(Config::default()), format!("{}:{}", self.host, self.port), @@ -88,19 +92,19 @@ impl SshTunnel { Ok::<(), Error>(()) }); - self.is_connected = true; + self.socket_addr = Some(addr); Ok(addr) } pub async fn close(&mut self) -> Result<()> { self.tx.send(0)?; - self.is_connected = false; + self.socket_addr = None; Ok(()) } #[allow(unused)] pub fn is_connected(&self) -> bool { - self.is_connected + self.socket_addr.is_some() } } diff --git a/src/components/redis_cli.rs b/src/components/redis_cli.rs index 450e648..6be884f 100644 --- a/src/components/redis_cli.rs +++ b/src/components/redis_cli.rs @@ -22,6 +22,7 @@ pub struct RedisCli<'a> { min_desc_width: u16, max_desc_width: u16, max_desc_height: u16, + auto_suggestion: String, single_line_text_area: TextArea<'a>, table_state: TableState, scroll_state: ScrollbarState, @@ -46,7 +47,8 @@ impl RedisCli<'_> { max_menu_height: 11, min_desc_width: 35, max_desc_width: 50, - max_desc_height: 20, + max_desc_height: 25, + auto_suggestion: "".to_string(), single_line_text_area: text_area, table_state, scroll_state, @@ -95,6 +97,14 @@ impl Renderable for RedisCli<'_> { } frame.render_widget(&self.single_line_text_area, input_area); + let (input_len, auto_suggestion) = self.get_auto_suggestion(); + if !auto_suggestion.is_empty() { + let auto_suggestion_area = Rect { + x: input_area.x + input_len as u16, + ..input_area + }; + frame.render_widget(Text::raw(&auto_suggestion).style(Style::default().dim()), auto_suggestion_area); + } if self.show_menu && self.completion_items.len() > 0 { frame.render_widget(Clear::default(), menu_area); let vertical = Layout::vertical([Fill(1), Length(1)]).split(menu_area); @@ -183,6 +193,18 @@ impl Listenable for RedisCli<'_> { false } } + KeyEvent { code: KeyCode::Right, modifiers: KeyModifiers::NONE, .. } => { + let (input_len, auto_suggestion) = self.get_auto_suggestion(); + if !auto_suggestion.is_empty() { + let (_, cursor_x) = self.single_line_text_area.cursor(); + if cursor_x == input_len { + self.single_line_text_area.insert_str(auto_suggestion); + return Ok(true); + } + } + self.single_line_text_area.move_cursor(CursorMove::Forward); + true + } KeyEvent { code: KeyCode::Tab, .. } => { if !self.completion_items.is_empty() && self.show_menu { if let Some(selected) = self.table_state.selected() { @@ -250,10 +272,19 @@ impl RedisCli<'_> { self.single_line_text_area.lines().get(cursor_y).unwrap().clone() } + pub fn set_auto_suggestion(&mut self, s: impl Into) { + self.auto_suggestion = s.into(); + } + pub fn update_frame(&mut self, frame_height: u16, frame_width: u16) { self.frame_size = (frame_height, frame_width); } + fn get_auto_suggestion(&self) -> (usize, String) { + let len = self.get_input().len(); + (len, self.auto_suggestion.chars().skip(len).collect()) + } + fn hide_menu(&mut self) { self.show_menu = false; self.reset_state(); diff --git a/src/tabs/cli.rs b/src/tabs/cli.rs index acd6f38..d502fd0 100644 --- a/src/tabs/cli.rs +++ b/src/tabs/cli.rs @@ -165,6 +165,12 @@ impl Listenable for CliTab { self.input_throbber_state.calc_next(); let handled = self.redis_cli.handle_key_event(key_event)?; if handled { + let current_input = self.redis_cli.get_input(); + if let Some((_, history_cmd)) = self.history.iter().rfind(|(_, cmd)| cmd.to_lowercase().starts_with(¤t_input.to_lowercase())) { + self.redis_cli.set_auto_suggestion(history_cmd); + } else { + self.redis_cli.set_auto_suggestion(""); + } return Ok(true); } }