From bb32819b49118819d67257192b5a770cbd1cffde Mon Sep 17 00:00:00 2001 From: hlsxx Date: Thu, 11 Sep 2025 08:36:07 +0200 Subject: [PATCH 01/11] feat: popup + delete method (wip) --- src/ui.rs | 106 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 98 insertions(+), 8 deletions(-) diff --git a/src/ui.rs b/src/ui.rs index b473366..36a98c5 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,14 +1,12 @@ use anyhow::Result; use crossterm::{ - cursor::{Hide, Show}, - event::{ + cursor::{Hide, Show}, event::{ self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, - }, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + }, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen} }; use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; +use ratatui::layout::Flex; #[allow(clippy::wildcard_imports)] use ratatui::{prelude::*, widgets::*}; use std::{ @@ -22,7 +20,7 @@ use tui_input::backend::crossterm::EventHandler; use tui_input::Input; use unicode_width::UnicodeWidthStr; -use crate::{searchable::Searchable, ssh}; +use crate::{searchable::Searchable, ssh::{self, Host}}; const INFO_TEXT: &str = "(Esc) quit | (↑) move up | (↓) move down | (enter) select"; @@ -40,6 +38,40 @@ pub struct AppConfig { pub exit_after_ssh_session_ends: bool, } +#[derive(Default)] +struct PopupWindow { + is_active: bool, + host_index_to_delete: usize, + selected_button_index: usize +} + +impl PopupWindow { + fn host_index_to_delete(&self) -> usize { + self.host_index_to_delete + } + + fn is_active(&self) -> bool { + self.is_active + } + + fn show(&mut self, host_index: usize) { + self.host_index_to_delete = host_index; + self.is_active = true; + } + + fn close(&mut self) { + self.is_active = false; + } + + fn toggle(&mut self) { + self.is_active = !self.is_active; + } + + pub fn selected_button_index(&self) -> usize { + self.selected_button_index + } +} + pub struct App { config: AppConfig, @@ -50,6 +82,8 @@ pub struct App { table_columns_constraints: Vec, palette: tailwind::Palette, + + popup_window: PopupWindow } #[derive(PartialEq)] @@ -114,6 +148,8 @@ impl App { || matcher.fuzzy_match(&host.aliases, search_value).is_some() }, ), + + popup_window: PopupWindow::default() }; app.calculate_table_columns_constraints(); @@ -241,6 +277,10 @@ impl App { return Ok(AppKeyAction::Stop); } } + Delete => { + let host_index = self.table_state.selected().unwrap_or(0); + self.popup_window.show(host_index); + } _ => return Ok(AppKeyAction::Continue), } @@ -422,11 +462,13 @@ fn ui(f: &mut Frame, app: &mut App) { .split(f.area()); render_searchbar(f, app, rects[0]); - render_table(f, app, rects[1]); - render_footer(f, app, rects[2]); + if app.popup_window.is_active() { + render_popup(f, app); + } + let mut cursor_position = rects[0].as_position(); cursor_position.x += u16::try_from(app.search.cursor()).unwrap_or_default() + 4; cursor_position.y += 1; @@ -434,6 +476,54 @@ fn ui(f: &mut Frame, app: &mut App) { f.set_cursor_position(cursor_position); } +fn render_popup(f: &mut Frame, app: &mut App) { + let host_to_delete = &app.hosts[app.popup_window.host_index_to_delete()]; + + let area = f.area(); + + let block = Block::bordered() + .border_type(BorderType::Rounded) + .border_style(Style::new().fg(app.palette.c400)); + + let yes_style = if app.popup_window.selected_button_index() == 0 { + Style::default().fg(Color::Black).bg(Color::Green).add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::Green) + }; + + let no_style = if app.popup_window.selected_button_index() == 1 { + Style::default().fg(Color::Black).bg(Color::Red).add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::Red) + }; + + let text = Text::from(vec![ + Line::from(vec![ + Span::raw(format!("Delete `{}` record?", host_to_delete.name)), + ]) + .bold(), + Line::default(), + Line::from(vec![ + Span::styled(" Yes ", yes_style), + Span::raw(" "), // spacing + Span::styled(" No ", no_style), + ]), + ]); + + let p = Paragraph::new(text) + .block(block) + .alignment(Alignment::Center) + .wrap(Wrap { trim: true }); + + let vertical = Layout::vertical([Constraint::Percentage(20)]).flex(Flex::Center); + let horizontal = Layout::horizontal([Constraint::Percentage(20)]).flex(Flex::Center); + let [area] = vertical.areas(area); + let [area] = horizontal.areas(area); + + f.render_widget(Clear, area); + f.render_widget(p, area); +} + fn render_searchbar(f: &mut Frame, app: &mut App, area: Rect) { let info_footer = Paragraph::new(Line::from(app.search.value())).block( Block::default() From 9e0bde8803b3024895bbb0fafc3617d559cd5d62 Mon Sep 17 00:00:00 2001 From: hlsxx Date: Thu, 11 Sep 2025 15:19:12 +0200 Subject: [PATCH 02/11] feat: window trait + delete window --- src/main.rs | 1 + src/ui.rs | 103 +++++-------------------------------------- src/window/delete.rs | 103 +++++++++++++++++++++++++++++++++++++++++++ src/window/mod.rs | 38 ++++++++++++++++ 4 files changed, 152 insertions(+), 93 deletions(-) create mode 100644 src/window/delete.rs create mode 100644 src/window/mod.rs diff --git a/src/main.rs b/src/main.rs index 224495c..65b9637 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ pub mod searchable; pub mod ssh; pub mod ssh_config; pub mod ui; +pub mod window; use anyhow::Result; use clap::Parser; diff --git a/src/ui.rs b/src/ui.rs index 36a98c5..510dac6 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -6,7 +6,6 @@ use crossterm::{ }, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen} }; use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; -use ratatui::layout::Flex; #[allow(clippy::wildcard_imports)] use ratatui::{prelude::*, widgets::*}; use std::{ @@ -20,7 +19,7 @@ use tui_input::backend::crossterm::EventHandler; use tui_input::Input; use unicode_width::UnicodeWidthStr; -use crate::{searchable::Searchable, ssh::{self, Host}}; +use crate::{searchable::Searchable, ssh::{self}, window::{centered_rect, delete::ShowData as DeletePopupWindowShowData, DeletePopupWindow, PopupWindow}}; const INFO_TEXT: &str = "(Esc) quit | (↑) move up | (↓) move down | (enter) select"; @@ -38,52 +37,17 @@ pub struct AppConfig { pub exit_after_ssh_session_ends: bool, } -#[derive(Default)] -struct PopupWindow { - is_active: bool, - host_index_to_delete: usize, - selected_button_index: usize -} - -impl PopupWindow { - fn host_index_to_delete(&self) -> usize { - self.host_index_to_delete - } - - fn is_active(&self) -> bool { - self.is_active - } - - fn show(&mut self, host_index: usize) { - self.host_index_to_delete = host_index; - self.is_active = true; - } - - fn close(&mut self) { - self.is_active = false; - } - - fn toggle(&mut self) { - self.is_active = !self.is_active; - } - - pub fn selected_button_index(&self) -> usize { - self.selected_button_index - } -} - pub struct App { config: AppConfig, search: Input, table_state: TableState, - hosts: Searchable, table_columns_constraints: Vec, - palette: tailwind::Palette, - - popup_window: PopupWindow + pub(crate) hosts: Searchable, + pub(crate) palette: tailwind::Palette, + pub(crate) delete_popup_window: DeletePopupWindow } #[derive(PartialEq)] @@ -149,7 +113,7 @@ impl App { }, ), - popup_window: PopupWindow::default() + delete_popup_window: DeletePopupWindow::default() }; app.calculate_table_columns_constraints(); @@ -278,8 +242,9 @@ impl App { } } Delete => { - let host_index = self.table_state.selected().unwrap_or(0); - self.popup_window.show(host_index); + let host_to_delete_index = self.table_state.selected().unwrap_or(0); + let host_to_delete = self.hosts[host_to_delete_index].clone(); + self.delete_popup_window.show(DeletePopupWindowShowData::new(host_to_delete_index, host_to_delete)); } _ => return Ok(AppKeyAction::Continue), } @@ -465,8 +430,8 @@ fn ui(f: &mut Frame, app: &mut App) { render_table(f, app, rects[1]); render_footer(f, app, rects[2]); - if app.popup_window.is_active() { - render_popup(f, app); + if app.delete_popup_window.is_active() { + app.delete_popup_window.render(f); } let mut cursor_position = rects[0].as_position(); @@ -476,54 +441,6 @@ fn ui(f: &mut Frame, app: &mut App) { f.set_cursor_position(cursor_position); } -fn render_popup(f: &mut Frame, app: &mut App) { - let host_to_delete = &app.hosts[app.popup_window.host_index_to_delete()]; - - let area = f.area(); - - let block = Block::bordered() - .border_type(BorderType::Rounded) - .border_style(Style::new().fg(app.palette.c400)); - - let yes_style = if app.popup_window.selected_button_index() == 0 { - Style::default().fg(Color::Black).bg(Color::Green).add_modifier(Modifier::BOLD) - } else { - Style::default().fg(Color::Green) - }; - - let no_style = if app.popup_window.selected_button_index() == 1 { - Style::default().fg(Color::Black).bg(Color::Red).add_modifier(Modifier::BOLD) - } else { - Style::default().fg(Color::Red) - }; - - let text = Text::from(vec![ - Line::from(vec![ - Span::raw(format!("Delete `{}` record?", host_to_delete.name)), - ]) - .bold(), - Line::default(), - Line::from(vec![ - Span::styled(" Yes ", yes_style), - Span::raw(" "), // spacing - Span::styled(" No ", no_style), - ]), - ]); - - let p = Paragraph::new(text) - .block(block) - .alignment(Alignment::Center) - .wrap(Wrap { trim: true }); - - let vertical = Layout::vertical([Constraint::Percentage(20)]).flex(Flex::Center); - let horizontal = Layout::horizontal([Constraint::Percentage(20)]).flex(Flex::Center); - let [area] = vertical.areas(area); - let [area] = horizontal.areas(area); - - f.render_widget(Clear, area); - f.render_widget(p, area); -} - fn render_searchbar(f: &mut Frame, app: &mut App, area: Rect) { let info_footer = Paragraph::new(Line::from(app.search.value())).block( Block::default() diff --git a/src/window/delete.rs b/src/window/delete.rs new file mode 100644 index 0000000..47a9a6e --- /dev/null +++ b/src/window/delete.rs @@ -0,0 +1,103 @@ +use ratatui::{layout::{Alignment, Constraint, Direction, Layout}, style::{palette::tailwind::Palette, Color, Modifier, Style, Stylize}, text::{Line, Span, Text}, widgets::{Block, BorderType, Clear, Paragraph}, Frame}; + +use crate::ssh::Host; + +use super::{PopupWindow, centered_rect}; + +pub struct ShowData { + host_index_to_delete: usize, + host_to_delete: Host +} + +impl ShowData { + pub fn new(host_index_to_delete: usize, host_to_delete: Host) -> Self { + Self { + host_index_to_delete, + host_to_delete + } + } +} + + +#[derive(Default)] +pub struct DeletePopupWindow { + is_active: bool, + + selected_button_index: usize, + show_data: Option +} + +impl PopupWindow for DeletePopupWindow { + type ShowData = ShowData; + + fn is_active(&self) -> bool { + self.is_active + } + + fn show(&mut self, data: Self::ShowData) { + self.show_data = Some(ShowData::new(data.host_index_to_delete, data.host_to_delete)); + self.is_active = true; + } + + fn close(&mut self) { + self.is_active = false; + } + + fn toggle(&mut self) { + self.is_active = !self.is_active; + } + + fn render(&mut self, f: &mut Frame) { + if let Some(show_data) = &self.show_data { + let popup_area = centered_rect(50, 20, f.area()); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), + Constraint::Min(1), + Constraint::Length(3), + ]) + .margin(1) + .split(popup_area); + + let block = Block::bordered() + .border_type(BorderType::Rounded); + // .border_style(Style::new().fg(Palette:)); + + f.render_widget(Clear, popup_area); + f.render_widget(block, popup_area); + + let question = Paragraph::new(Text::from(vec![ + Line::from(vec![Span::raw(format!("Delete `{}` record?", show_data.host_to_delete.name))]).bold(), + ])) + .alignment(Alignment::Center); + + f.render_widget(question, chunks[0]); + + let yes_style = if self.selected_button_index == 0 { + Style::default().fg(Color::Black).bg(Color::Green).add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::Green) + }; + + let no_style = if self.selected_button_index == 1 { + Style::default().fg(Color::Black).bg(Color::Red).add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::Red) + }; + + let buttons_line = Line::from(vec![ + Span::styled(" Yes ", yes_style), + Span::raw(" "), + Span::styled(" No ", no_style), + ]); + + let buttons = Paragraph::new(Text::from(buttons_line)) + .alignment(Alignment::Center); + + f.render_widget(buttons, chunks[2]); + } + } + +} diff --git a/src/window/mod.rs b/src/window/mod.rs new file mode 100644 index 0000000..90cec30 --- /dev/null +++ b/src/window/mod.rs @@ -0,0 +1,38 @@ +pub mod delete; + +pub use delete::DeletePopupWindow; +use ratatui::{layout::{Constraint, Direction, Layout, Rect}, Frame}; + +use crate::ui::App; + +pub trait PopupWindow { + type ShowData; + + fn is_active(&self) -> bool; + fn show(&mut self, data: Self::ShowData); + fn close(&mut self); + fn toggle(&mut self); + fn render(&mut self, f: &mut Frame); +} + +pub fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { + let vertical = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage((100 - percent_y) / 2), + Constraint::Percentage(percent_y), + Constraint::Percentage((100 - percent_y) / 2), + ]) + .split(r); + + let horizontal = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage((100 - percent_x) / 2), + Constraint::Percentage(percent_x), + Constraint::Percentage((100 - percent_x) / 2), + ]) + .split(vertical[1]); + + horizontal[1] +} From 63139e13ea79d9f393b5f3b074ba2e0e6ac6ceed Mon Sep 17 00:00:00 2001 From: hlsxx Date: Thu, 11 Sep 2025 15:28:08 +0200 Subject: [PATCH 03/11] style: cargo fmt --- src/ui.rs | 36 ++++++++++++++++++++++----- src/window/delete.rs | 59 ++++++++++++++++++++++++++++++-------------- src/window/mod.rs | 7 +++--- 3 files changed, 75 insertions(+), 27 deletions(-) diff --git a/src/ui.rs b/src/ui.rs index 510dac6..9ac73df 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,9 +1,12 @@ use anyhow::Result; use crossterm::{ - cursor::{Hide, Show}, event::{ + cursor::{Hide, Show}, + event::{ self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, - }, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen} + }, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; #[allow(clippy::wildcard_imports)] @@ -19,7 +22,14 @@ use tui_input::backend::crossterm::EventHandler; use tui_input::Input; use unicode_width::UnicodeWidthStr; -use crate::{searchable::Searchable, ssh::{self}, window::{centered_rect, delete::ShowData as DeletePopupWindowShowData, DeletePopupWindow, PopupWindow}}; +use crate::{ + searchable::Searchable, + ssh::{self}, + window::{ + centered_rect, delete::ShowData as DeletePopupWindowShowData, DeletePopupWindow, + PopupWindow, + }, +}; const INFO_TEXT: &str = "(Esc) quit | (↑) move up | (↓) move down | (enter) select"; @@ -47,7 +57,7 @@ pub struct App { pub(crate) hosts: Searchable, pub(crate) palette: tailwind::Palette, - pub(crate) delete_popup_window: DeletePopupWindow + pub(crate) delete_popup_window: DeletePopupWindow, } #[derive(PartialEq)] @@ -113,7 +123,7 @@ impl App { }, ), - delete_popup_window: DeletePopupWindow::default() + delete_popup_window: DeletePopupWindow::default(), }; app.calculate_table_columns_constraints(); @@ -244,7 +254,21 @@ impl App { Delete => { let host_to_delete_index = self.table_state.selected().unwrap_or(0); let host_to_delete = self.hosts[host_to_delete_index].clone(); - self.delete_popup_window.show(DeletePopupWindowShowData::new(host_to_delete_index, host_to_delete)); + self.delete_popup_window + .show(DeletePopupWindowShowData::new( + host_to_delete_index, + host_to_delete, + )); + } + Left => { + if self.delete_popup_window.is_active() { + self.delete_popup_window.previous(); + } + } + Right => { + if self.delete_popup_window.is_active() { + self.delete_popup_window.next(); + } } _ => return Ok(AppKeyAction::Continue), } diff --git a/src/window/delete.rs b/src/window/delete.rs index 47a9a6e..8097358 100644 --- a/src/window/delete.rs +++ b/src/window/delete.rs @@ -1,30 +1,35 @@ -use ratatui::{layout::{Alignment, Constraint, Direction, Layout}, style::{palette::tailwind::Palette, Color, Modifier, Style, Stylize}, text::{Line, Span, Text}, widgets::{Block, BorderType, Clear, Paragraph}, Frame}; +use ratatui::{ + layout::{Alignment, Constraint, Direction, Layout}, + style::{palette::tailwind::Palette, Color, Modifier, Style, Stylize}, + text::{Line, Span, Text}, + widgets::{Block, BorderType, Clear, Paragraph}, + Frame, +}; use crate::ssh::Host; -use super::{PopupWindow, centered_rect}; +use super::{centered_rect, PopupWindow}; pub struct ShowData { host_index_to_delete: usize, - host_to_delete: Host + host_to_delete: Host, } impl ShowData { pub fn new(host_index_to_delete: usize, host_to_delete: Host) -> Self { Self { host_index_to_delete, - host_to_delete + host_to_delete, } } } - #[derive(Default)] pub struct DeletePopupWindow { is_active: bool, selected_button_index: usize, - show_data: Option + show_data: Option, } impl PopupWindow for DeletePopupWindow { @@ -35,7 +40,10 @@ impl PopupWindow for DeletePopupWindow { } fn show(&mut self, data: Self::ShowData) { - self.show_data = Some(ShowData::new(data.host_index_to_delete, data.host_to_delete)); + self.show_data = Some(ShowData::new( + data.host_index_to_delete, + data.host_to_delete, + )); self.is_active = true; } @@ -61,28 +69,35 @@ impl PopupWindow for DeletePopupWindow { .margin(1) .split(popup_area); - let block = Block::bordered() - .border_type(BorderType::Rounded); - // .border_style(Style::new().fg(Palette:)); + let block = Block::bordered().border_type(BorderType::Rounded); + // .border_style(Style::new().fg(Palette:)); f.render_widget(Clear, popup_area); f.render_widget(block, popup_area); - let question = Paragraph::new(Text::from(vec![ - Line::from(vec![Span::raw(format!("Delete `{}` record?", show_data.host_to_delete.name))]).bold(), - ])) - .alignment(Alignment::Center); + let question = Paragraph::new(Text::from(vec![Line::from(vec![Span::raw(format!( + "Delete `{}` record?", + show_data.host_to_delete.name + ))]) + .bold()])) + .alignment(Alignment::Center); f.render_widget(question, chunks[0]); let yes_style = if self.selected_button_index == 0 { - Style::default().fg(Color::Black).bg(Color::Green).add_modifier(Modifier::BOLD) + Style::default() + .fg(Color::Black) + .bg(Color::Green) + .add_modifier(Modifier::BOLD) } else { Style::default().fg(Color::Green) }; let no_style = if self.selected_button_index == 1 { - Style::default().fg(Color::Black).bg(Color::Red).add_modifier(Modifier::BOLD) + Style::default() + .fg(Color::Black) + .bg(Color::Red) + .add_modifier(Modifier::BOLD) } else { Style::default().fg(Color::Red) }; @@ -93,11 +108,19 @@ impl PopupWindow for DeletePopupWindow { Span::styled(" No ", no_style), ]); - let buttons = Paragraph::new(Text::from(buttons_line)) - .alignment(Alignment::Center); + let buttons = Paragraph::new(Text::from(buttons_line)).alignment(Alignment::Center); f.render_widget(buttons, chunks[2]); } } +} +impl DeletePopupWindow { + pub fn next(&mut self) { + self.selected_button_index = 1; + } + + pub fn previous(&mut self) { + self.selected_button_index = 0; + } } diff --git a/src/window/mod.rs b/src/window/mod.rs index 90cec30..dc75f64 100644 --- a/src/window/mod.rs +++ b/src/window/mod.rs @@ -1,9 +1,10 @@ pub mod delete; pub use delete::DeletePopupWindow; -use ratatui::{layout::{Constraint, Direction, Layout, Rect}, Frame}; - -use crate::ui::App; +use ratatui::{ + layout::{Constraint, Direction, Layout, Rect}, + Frame, +}; pub trait PopupWindow { type ShowData; From 94d4dbfd9613f2634a4d932ee9ef32a4da4fbdc9 Mon Sep 17 00:00:00 2001 From: hlsxx Date: Thu, 11 Sep 2025 15:36:42 +0200 Subject: [PATCH 04/11] refactor: close -> hide method in window --- src/ui.rs | 13 ++++++++++--- src/window/delete.rs | 2 +- src/window/mod.rs | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/ui.rs b/src/ui.rs index 9ac73df..260816f 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -26,12 +26,12 @@ use crate::{ searchable::Searchable, ssh::{self}, window::{ - centered_rect, delete::ShowData as DeletePopupWindowShowData, DeletePopupWindow, + delete::ShowData as DeletePopupWindowShowData, DeletePopupWindow, PopupWindow, }, }; -const INFO_TEXT: &str = "(Esc) quit | (↑) move up | (↓) move down | (enter) select"; +const INFO_TEXT: &str = "(Esc) quit | (↑) move up | (↓) move down | (enter) select | (Del) delete"; #[derive(Clone)] pub struct AppConfig { @@ -208,7 +208,14 @@ impl App { } match key.code { - Esc => return Ok(AppKeyAction::Stop), + Esc => { + // Hide popup if is showed else exit app + if self.delete_popup_window.is_active() { + self.delete_popup_window.hide(); + } else { + return Ok(AppKeyAction::Stop) + } + }, Down => self.next(), Up => self.previous(), Home => self.table_state.select(Some(0)), diff --git a/src/window/delete.rs b/src/window/delete.rs index 8097358..29fe473 100644 --- a/src/window/delete.rs +++ b/src/window/delete.rs @@ -47,7 +47,7 @@ impl PopupWindow for DeletePopupWindow { self.is_active = true; } - fn close(&mut self) { + fn hide(&mut self) { self.is_active = false; } diff --git a/src/window/mod.rs b/src/window/mod.rs index dc75f64..135a51d 100644 --- a/src/window/mod.rs +++ b/src/window/mod.rs @@ -11,7 +11,7 @@ pub trait PopupWindow { fn is_active(&self) -> bool; fn show(&mut self, data: Self::ShowData); - fn close(&mut self); + fn hide(&mut self); fn toggle(&mut self); fn render(&mut self, f: &mut Frame); } From c4f5bb15de144270b3f73c7a0641fb81fd571a2b Mon Sep 17 00:00:00 2001 From: hlsxx Date: Fri, 12 Sep 2025 19:03:14 +0200 Subject: [PATCH 05/11] feat: on_key_press of popup window --- src/ui.rs | 32 +++++++++++--------------------- src/window/delete.rs | 35 +++++++++++++++++++++++++++++------ src/window/mod.rs | 12 ++++++++++-- 3 files changed, 50 insertions(+), 29 deletions(-) diff --git a/src/ui.rs b/src/ui.rs index 260816f..30a283d 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -61,7 +61,7 @@ pub struct App { } #[derive(PartialEq)] -enum AppKeyAction { +pub enum AppKeyAction { Ok, Stop, Continue, @@ -171,8 +171,10 @@ impl App { } } - self.search.handle_event(&ev); - self.hosts.search(self.search.value()); + if !self.delete_popup_window.is_active() { + self.search.handle_event(&ev); + self.hosts.search(self.search.value()); + } let selected = self.table_state.selected().unwrap_or(0); if selected >= self.hosts.len() { @@ -198,6 +200,11 @@ impl App { #[allow(clippy::enum_glob_use)] use KeyCode::*; + // If Popup Window is active `consume` key events + if self.delete_popup_window.is_active() { + return self.delete_popup_window.on_key_press(key); + } + let is_ctrl_pressed = key.modifiers.contains(KeyModifiers::CONTROL); if is_ctrl_pressed { @@ -208,14 +215,7 @@ impl App { } match key.code { - Esc => { - // Hide popup if is showed else exit app - if self.delete_popup_window.is_active() { - self.delete_popup_window.hide(); - } else { - return Ok(AppKeyAction::Stop) - } - }, + Esc => return Ok(AppKeyAction::Stop), Down => self.next(), Up => self.previous(), Home => self.table_state.select(Some(0)), @@ -267,16 +267,6 @@ impl App { host_to_delete, )); } - Left => { - if self.delete_popup_window.is_active() { - self.delete_popup_window.previous(); - } - } - Right => { - if self.delete_popup_window.is_active() { - self.delete_popup_window.next(); - } - } _ => return Ok(AppKeyAction::Continue), } diff --git a/src/window/delete.rs b/src/window/delete.rs index 29fe473..0e274e8 100644 --- a/src/window/delete.rs +++ b/src/window/delete.rs @@ -1,12 +1,10 @@ +use anyhow::Result; +use crossterm::event::{KeyEvent, KeyCode}; use ratatui::{ - layout::{Alignment, Constraint, Direction, Layout}, - style::{palette::tailwind::Palette, Color, Modifier, Style, Stylize}, - text::{Line, Span, Text}, - widgets::{Block, BorderType, Clear, Paragraph}, - Frame, + layout::{Alignment, Constraint, Direction, Layout}, prelude::Backend, style::{palette::tailwind::Palette, Color, Modifier, Style, Stylize}, text::{Line, Span, Text}, widgets::{Block, BorderType, Clear, Paragraph}, Frame }; -use crate::ssh::Host; +use crate::{ssh::Host, ui::AppKeyAction}; use super::{centered_rect, PopupWindow}; @@ -35,6 +33,31 @@ pub struct DeletePopupWindow { impl PopupWindow for DeletePopupWindow { type ShowData = ShowData; + fn on_key_press( + &mut self, + key: KeyEvent, + ) -> Result { + #[allow(clippy::enum_glob_use)] + use KeyCode::*; + + match key.code { + Esc => self.hide(), + Left => { + if self.is_active() { + self.previous(); + } + } + Right => { + if self.is_active() { + self.next(); + } + } + _ => return Ok(AppKeyAction::Continue), + } + + Ok(AppKeyAction::Ok) + } + fn is_active(&self) -> bool { self.is_active } diff --git a/src/window/mod.rs b/src/window/mod.rs index 135a51d..5f3af0e 100644 --- a/src/window/mod.rs +++ b/src/window/mod.rs @@ -1,14 +1,22 @@ pub mod delete; +use anyhow::Result; +use crossterm::event::KeyEvent; pub use delete::DeletePopupWindow; use ratatui::{ - layout::{Constraint, Direction, Layout, Rect}, - Frame, + layout::{Constraint, Direction, Layout, Rect}, prelude::Backend, Frame }; +use crate::ui::AppKeyAction; + pub trait PopupWindow { type ShowData; + fn on_key_press( + &mut self, + key: KeyEvent, + ) -> Result; + fn is_active(&self) -> bool; fn show(&mut self, data: Self::ShowData); fn hide(&mut self); From 65126fadd72189da47496a19b419972f4753130d Mon Sep 17 00:00:00 2001 From: hlsxx Date: Sat, 13 Sep 2025 18:43:58 +0200 Subject: [PATCH 06/11] feat: searchable items method --- src/searchable.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/searchable.rs b/src/searchable.rs index a0d8753..cbea1b1 100644 --- a/src/searchable.rs +++ b/src/searchable.rs @@ -60,6 +60,10 @@ where pub fn iter(&self) -> std::slice::Iter { self.filtered.iter() } + + pub fn items(&self) -> Vec { + self.vec.clone() + } } impl<'a, T> IntoIterator for &'a Searchable From b2a44212ee39bf07582fad681b00c66b71ca33f9 Mon Sep 17 00:00:00 2001 From: hlsxx Date: Sat, 13 Sep 2025 18:44:17 +0200 Subject: [PATCH 07/11] feat: parser save into file + host display --- src/ssh.rs | 28 +++++++++++++++++++++++++++- src/ssh_config/parser.rs | 28 ++++++++++++++++++++++++++-- src/ui.rs | 3 ++- src/window/delete.rs | 26 ++++++++++++++++++-------- 4 files changed, 73 insertions(+), 12 deletions(-) diff --git a/src/ssh.rs b/src/ssh.rs index cc79745..9a010f3 100644 --- a/src/ssh.rs +++ b/src/ssh.rs @@ -2,7 +2,7 @@ use anyhow::anyhow; use handlebars::Handlebars; use itertools::Itertools; use serde::Serialize; -use std::collections::VecDeque; +use std::{collections::VecDeque, fmt::Display}; use std::process::Command; use crate::ssh_config::{self, parser_error::ParseError, HostVecExt}; @@ -48,6 +48,32 @@ impl Host { } } +impl std::fmt::Display for Host { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.aliases.is_empty() { + writeln!(f, "Host {}", self.name)?; + } else { + writeln!(f, "Host {} {}", self.name, self.aliases)?; + } + + writeln!(f, " HostName {}", self.destination)?; + + if let Some(user) = &self.user { + writeln!(f, " User {}", user)?; + } + + if let Some(port) = &self.port { + writeln!(f, " Port {}", port)?; + } + + if let Some(proxy) = &self.proxy_command { + writeln!(f, " ProxyCommand {}", proxy)?; + } + + Ok(()) + } +} + #[derive(Debug)] pub enum ParseConfigError { Io(std::io::Error), diff --git a/src/ssh_config/parser.rs b/src/ssh_config/parser.rs index b455ef5..3891f02 100644 --- a/src/ssh_config/parser.rs +++ b/src/ssh_config/parser.rs @@ -1,10 +1,15 @@ +use anyhow::Result; use glob::glob; +use std::env; use std::fs::File; -use std::io::BufRead; -use std::io::BufReader; +use std::fs::OpenOptions; +use std::io::{BufRead, BufReader, Write}; use std::path::Path; +use std::path::PathBuf; use std::str::FromStr; +use crate::ssh; + use super::host::Entry; use super::parser_error::InvalidIncludeError; use super::parser_error::InvalidIncludeErrorDetails; @@ -57,6 +62,25 @@ impl Parser { Ok(hosts) } + pub fn save_into_file(hosts: Vec<&ssh::Host>) -> Result<()> { + let home = env::var("HOME").unwrap(); + + let mut path = PathBuf::from(home); + path.push(".ssh/config"); + + let mut file = OpenOptions::new() + .write(true) + .truncate(true) + .open(path)?; + + for host in hosts { + writeln!(file, "{}", host)?; + writeln!(file)?; + } + + Ok(()) + } + fn parse_raw(&self, reader: &mut impl BufRead) -> Result<(Host, Vec), ParseError> { let mut parent_host = Host::new(Vec::new()); let mut hosts = Vec::new(); diff --git a/src/ui.rs b/src/ui.rs index 30a283d..d2e400d 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -111,7 +111,7 @@ impl App { palette: tailwind::BLUE, hosts: Searchable::new( - hosts, + hosts.clone(), &search_input, move |host: &&ssh::Host, search_value: &str| -> bool { search_value.is_empty() @@ -263,6 +263,7 @@ impl App { let host_to_delete = self.hosts[host_to_delete_index].clone(); self.delete_popup_window .show(DeletePopupWindowShowData::new( + self.hosts.items(), host_to_delete_index, host_to_delete, )); diff --git a/src/window/delete.rs b/src/window/delete.rs index 0e274e8..e592216 100644 --- a/src/window/delete.rs +++ b/src/window/delete.rs @@ -4,19 +4,21 @@ use ratatui::{ layout::{Alignment, Constraint, Direction, Layout}, prelude::Backend, style::{palette::tailwind::Palette, Color, Modifier, Style, Stylize}, text::{Line, Span, Text}, widgets::{Block, BorderType, Clear, Paragraph}, Frame }; -use crate::{ssh::Host, ui::AppKeyAction}; +use crate::{ssh::Host, ssh_config, ui::AppKeyAction}; use super::{centered_rect, PopupWindow}; pub struct ShowData { - host_index_to_delete: usize, + hosts: Vec, + host_to_delete_index: usize, host_to_delete: Host, } impl ShowData { - pub fn new(host_index_to_delete: usize, host_to_delete: Host) -> Self { + pub fn new(hosts: Vec, host_to_delete_index: usize, host_to_delete: Host) -> Self { Self { - host_index_to_delete, + hosts, + host_to_delete_index, host_to_delete, } } @@ -41,6 +43,17 @@ impl PopupWindow for DeletePopupWindow { use KeyCode::*; match key.code { + Enter => { + let show_data = self.show_data.as_ref().unwrap(); + let new_hosts = show_data.hosts + .iter() + .enumerate() + .filter(|(index, _)| *index != show_data.host_to_delete_index) + .map(|(_, host)| host) + .collect::>(); + + ssh_config::Parser::save_into_file(new_hosts)?; + } Esc => self.hide(), Left => { if self.is_active() { @@ -63,10 +76,7 @@ impl PopupWindow for DeletePopupWindow { } fn show(&mut self, data: Self::ShowData) { - self.show_data = Some(ShowData::new( - data.host_index_to_delete, - data.host_to_delete, - )); + self.show_data = Some(data); self.is_active = true; } From 065242295d4ee72fdea5c85e16dee1464f895685 Mon Sep 17 00:00:00 2001 From: hlsxx Date: Sat, 13 Sep 2025 21:01:30 +0200 Subject: [PATCH 08/11] feat: save into file and refresh hosts --- src/ssh_config/parser.rs | 2 +- src/ui.rs | 9 +++++++-- src/window/delete.rs | 28 ++++++++++++++++++++++++++-- src/window/mod.rs | 2 ++ 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/ssh_config/parser.rs b/src/ssh_config/parser.rs index 3891f02..64c454d 100644 --- a/src/ssh_config/parser.rs +++ b/src/ssh_config/parser.rs @@ -62,7 +62,7 @@ impl Parser { Ok(hosts) } - pub fn save_into_file(hosts: Vec<&ssh::Host>) -> Result<()> { + pub fn save_into_file(hosts: Vec) -> Result<()> { let home = env::var("HOME").unwrap(); let mut path = PathBuf::from(home); diff --git a/src/ui.rs b/src/ui.rs index d2e400d..d398d34 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -26,7 +26,9 @@ use crate::{ searchable::Searchable, ssh::{self}, window::{ - delete::ShowData as DeletePopupWindowShowData, DeletePopupWindow, + delete::ShowData as DeletePopupWindowShowData, + delete::OnKeyPressData as DeletePopupWindowOnKeyPressData, + DeletePopupWindow, PopupWindow, }, }; @@ -202,7 +204,10 @@ impl App { // If Popup Window is active `consume` key events if self.delete_popup_window.is_active() { - return self.delete_popup_window.on_key_press(key); + let mut on_key_press_data = DeletePopupWindowOnKeyPressData::new(self.hosts.items()); + let res = self.delete_popup_window.on_key_press(key, &mut on_key_press_data); + self.hosts = Searchable::new(on_key_press_data.hosts, "", |_, _| false); + return res; } let is_ctrl_pressed = key.modifiers.contains(KeyModifiers::CONTROL); diff --git a/src/window/delete.rs b/src/window/delete.rs index e592216..b112d7b 100644 --- a/src/window/delete.rs +++ b/src/window/delete.rs @@ -1,3 +1,5 @@ +use std::ops::DerefMut; + use anyhow::Result; use crossterm::event::{KeyEvent, KeyCode}; use ratatui::{ @@ -24,6 +26,18 @@ impl ShowData { } } +pub struct OnKeyPressData { + pub hosts: Vec +} + +impl OnKeyPressData { + pub fn new(hosts: Vec) -> Self { + Self { + hosts + } + } +} + #[derive(Default)] pub struct DeletePopupWindow { is_active: bool, @@ -34,25 +48,35 @@ pub struct DeletePopupWindow { impl PopupWindow for DeletePopupWindow { type ShowData = ShowData; + type OnKeyPressData = OnKeyPressData; fn on_key_press( &mut self, key: KeyEvent, + data: &mut Self::OnKeyPressData ) -> Result { #[allow(clippy::enum_glob_use)] use KeyCode::*; match key.code { Enter => { + if self.selected_button_index == 1 { + self.hide(); + return Ok(AppKeyAction::Continue) + } + let show_data = self.show_data.as_ref().unwrap(); let new_hosts = show_data.hosts .iter() .enumerate() .filter(|(index, _)| *index != show_data.host_to_delete_index) - .map(|(_, host)| host) - .collect::>(); + .map(|(_, host)| host.clone()) + .collect::>(); + + data.hosts = new_hosts.clone(); ssh_config::Parser::save_into_file(new_hosts)?; + self.hide(); } Esc => self.hide(), Left => { diff --git a/src/window/mod.rs b/src/window/mod.rs index 5f3af0e..52ff485 100644 --- a/src/window/mod.rs +++ b/src/window/mod.rs @@ -11,10 +11,12 @@ use crate::ui::AppKeyAction; pub trait PopupWindow { type ShowData; + type OnKeyPressData; fn on_key_press( &mut self, key: KeyEvent, + data: &mut Self::OnKeyPressData ) -> Result; fn is_active(&self) -> bool; From 57b23b722f9bccea4e56be779052b3c285ef1c6a Mon Sep 17 00:00:00 2001 From: hlsxx Date: Sun, 14 Sep 2025 17:00:53 +0200 Subject: [PATCH 09/11] style: code style --- src/ssh.rs | 2 +- src/ssh_config/parser.rs | 11 ++++------- src/ui.rs | 9 +++++---- src/window/delete.rs | 22 +++++++++++++--------- src/window/mod.rs | 6 ++++-- 5 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/ssh.rs b/src/ssh.rs index 9a010f3..e07dd2d 100644 --- a/src/ssh.rs +++ b/src/ssh.rs @@ -2,8 +2,8 @@ use anyhow::anyhow; use handlebars::Handlebars; use itertools::Itertools; use serde::Serialize; -use std::{collections::VecDeque, fmt::Display}; use std::process::Command; +use std::{collections::VecDeque, fmt::Display}; use crate::ssh_config::{self, parser_error::ParseError, HostVecExt}; diff --git a/src/ssh_config/parser.rs b/src/ssh_config/parser.rs index 64c454d..3d6e0b1 100644 --- a/src/ssh_config/parser.rs +++ b/src/ssh_config/parser.rs @@ -63,15 +63,12 @@ impl Parser { } pub fn save_into_file(hosts: Vec) -> Result<()> { - let home = env::var("HOME").unwrap(); + let normalized_path = shellexpand::tilde("~/.ssh/config").to_string(); + let path = std::fs::canonicalize(normalized_path)?; - let mut path = PathBuf::from(home); - path.push(".ssh/config"); + let path = PathBuf::from(path); - let mut file = OpenOptions::new() - .write(true) - .truncate(true) - .open(path)?; + let mut file = OpenOptions::new().write(true).truncate(true).open(path)?; for host in hosts { writeln!(file, "{}", host)?; diff --git a/src/ui.rs b/src/ui.rs index d398d34..a875f22 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -26,10 +26,8 @@ use crate::{ searchable::Searchable, ssh::{self}, window::{ - delete::ShowData as DeletePopupWindowShowData, delete::OnKeyPressData as DeletePopupWindowOnKeyPressData, - DeletePopupWindow, - PopupWindow, + delete::ShowData as DeletePopupWindowShowData, DeletePopupWindow, PopupWindow, }, }; @@ -205,7 +203,10 @@ impl App { // If Popup Window is active `consume` key events if self.delete_popup_window.is_active() { let mut on_key_press_data = DeletePopupWindowOnKeyPressData::new(self.hosts.items()); - let res = self.delete_popup_window.on_key_press(key, &mut on_key_press_data); + + let res = self + .delete_popup_window + .on_key_press(key, &mut on_key_press_data); self.hosts = Searchable::new(on_key_press_data.hosts, "", |_, _| false); return res; } diff --git a/src/window/delete.rs b/src/window/delete.rs index b112d7b..c2cd9bd 100644 --- a/src/window/delete.rs +++ b/src/window/delete.rs @@ -1,9 +1,14 @@ use std::ops::DerefMut; use anyhow::Result; -use crossterm::event::{KeyEvent, KeyCode}; +use crossterm::event::{KeyCode, KeyEvent}; use ratatui::{ - layout::{Alignment, Constraint, Direction, Layout}, prelude::Backend, style::{palette::tailwind::Palette, Color, Modifier, Style, Stylize}, text::{Line, Span, Text}, widgets::{Block, BorderType, Clear, Paragraph}, Frame + layout::{Alignment, Constraint, Direction, Layout}, + prelude::Backend, + style::{palette::tailwind::Palette, Color, Modifier, Style, Stylize}, + text::{Line, Span, Text}, + widgets::{Block, BorderType, Clear, Paragraph}, + Frame, }; use crate::{ssh::Host, ssh_config, ui::AppKeyAction}; @@ -27,14 +32,12 @@ impl ShowData { } pub struct OnKeyPressData { - pub hosts: Vec + pub hosts: Vec, } impl OnKeyPressData { pub fn new(hosts: Vec) -> Self { - Self { - hosts - } + Self { hosts } } } @@ -53,7 +56,7 @@ impl PopupWindow for DeletePopupWindow { fn on_key_press( &mut self, key: KeyEvent, - data: &mut Self::OnKeyPressData + data: &mut Self::OnKeyPressData, ) -> Result { #[allow(clippy::enum_glob_use)] use KeyCode::*; @@ -62,11 +65,12 @@ impl PopupWindow for DeletePopupWindow { Enter => { if self.selected_button_index == 1 { self.hide(); - return Ok(AppKeyAction::Continue) + return Ok(AppKeyAction::Continue); } let show_data = self.show_data.as_ref().unwrap(); - let new_hosts = show_data.hosts + let new_hosts = show_data + .hosts .iter() .enumerate() .filter(|(index, _)| *index != show_data.host_to_delete_index) diff --git a/src/window/mod.rs b/src/window/mod.rs index 52ff485..8a40401 100644 --- a/src/window/mod.rs +++ b/src/window/mod.rs @@ -4,7 +4,9 @@ use anyhow::Result; use crossterm::event::KeyEvent; pub use delete::DeletePopupWindow; use ratatui::{ - layout::{Constraint, Direction, Layout, Rect}, prelude::Backend, Frame + layout::{Constraint, Direction, Layout, Rect}, + prelude::Backend, + Frame, }; use crate::ui::AppKeyAction; @@ -16,7 +18,7 @@ pub trait PopupWindow { fn on_key_press( &mut self, key: KeyEvent, - data: &mut Self::OnKeyPressData + data: &mut Self::OnKeyPressData, ) -> Result; fn is_active(&self) -> bool; From 5b019fe152e3dd6010ec2d7bc327dd8978d85ba9 Mon Sep 17 00:00:00 2001 From: hlsxx Date: Mon, 15 Sep 2025 15:57:15 +0200 Subject: [PATCH 10/11] feat: config_path into key press --- src/ssh.rs | 2 +- src/ssh_config/parser.rs | 7 ++----- src/ui.rs | 7 ++++++- src/window/delete.rs | 19 ++++++++++++------- src/window/mod.rs | 1 - 5 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/ssh.rs b/src/ssh.rs index e07dd2d..255a10a 100644 --- a/src/ssh.rs +++ b/src/ssh.rs @@ -2,8 +2,8 @@ use anyhow::anyhow; use handlebars::Handlebars; use itertools::Itertools; use serde::Serialize; +use std::collections::VecDeque; use std::process::Command; -use std::{collections::VecDeque, fmt::Display}; use crate::ssh_config::{self, parser_error::ParseError, HostVecExt}; diff --git a/src/ssh_config/parser.rs b/src/ssh_config/parser.rs index 3d6e0b1..63674c0 100644 --- a/src/ssh_config/parser.rs +++ b/src/ssh_config/parser.rs @@ -1,6 +1,5 @@ use anyhow::Result; use glob::glob; -use std::env; use std::fs::File; use std::fs::OpenOptions; use std::io::{BufRead, BufReader, Write}; @@ -62,12 +61,10 @@ impl Parser { Ok(hosts) } - pub fn save_into_file(hosts: Vec) -> Result<()> { - let normalized_path = shellexpand::tilde("~/.ssh/config").to_string(); + pub fn save_into_file(hosts: Vec, path: &str) -> Result<()> { + let normalized_path = shellexpand::tilde(&path).to_string(); let path = std::fs::canonicalize(normalized_path)?; - let path = PathBuf::from(path); - let mut file = OpenOptions::new().write(true).truncate(true).open(path)?; for host in hosts { diff --git a/src/ui.rs b/src/ui.rs index a875f22..95080e1 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -202,12 +202,17 @@ impl App { // If Popup Window is active `consume` key events if self.delete_popup_window.is_active() { - let mut on_key_press_data = DeletePopupWindowOnKeyPressData::new(self.hosts.items()); + let mut on_key_press_data = DeletePopupWindowOnKeyPressData::new( + self.config.config_paths.clone(), + self.hosts.items(), + ); let res = self .delete_popup_window .on_key_press(key, &mut on_key_press_data); + self.hosts = Searchable::new(on_key_press_data.hosts, "", |_, _| false); + return res; } diff --git a/src/window/delete.rs b/src/window/delete.rs index c2cd9bd..73df02b 100644 --- a/src/window/delete.rs +++ b/src/window/delete.rs @@ -1,11 +1,8 @@ -use std::ops::DerefMut; - use anyhow::Result; use crossterm::event::{KeyCode, KeyEvent}; use ratatui::{ layout::{Alignment, Constraint, Direction, Layout}, - prelude::Backend, - style::{palette::tailwind::Palette, Color, Modifier, Style, Stylize}, + style::{Color, Modifier, Style, Stylize}, text::{Line, Span, Text}, widgets::{Block, BorderType, Clear, Paragraph}, Frame, @@ -32,12 +29,16 @@ impl ShowData { } pub struct OnKeyPressData { + pub config_paths: Vec, pub hosts: Vec, } impl OnKeyPressData { - pub fn new(hosts: Vec) -> Self { - Self { hosts } + pub fn new(config_paths: Vec, hosts: Vec) -> Self { + Self { + config_paths, + hosts, + } } } @@ -68,7 +69,11 @@ impl PopupWindow for DeletePopupWindow { return Ok(AppKeyAction::Continue); } + // Select first path from the config + // let path = &data.config_paths.get(0).unwrap(); + let path = "~/.ssh/config"; let show_data = self.show_data.as_ref().unwrap(); + let new_hosts = show_data .hosts .iter() @@ -79,7 +84,7 @@ impl PopupWindow for DeletePopupWindow { data.hosts = new_hosts.clone(); - ssh_config::Parser::save_into_file(new_hosts)?; + ssh_config::Parser::save_into_file(new_hosts, path)?; self.hide(); } Esc => self.hide(), diff --git a/src/window/mod.rs b/src/window/mod.rs index 8a40401..7b53364 100644 --- a/src/window/mod.rs +++ b/src/window/mod.rs @@ -5,7 +5,6 @@ use crossterm::event::KeyEvent; pub use delete::DeletePopupWindow; use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, - prelude::Backend, Frame, }; From 64a0d0e03b564a70d6136945fad49ffd5a7f5ff7 Mon Sep 17 00:00:00 2001 From: hlsxx Date: Mon, 15 Sep 2025 16:24:43 +0200 Subject: [PATCH 11/11] chore: removed PathBuf from import --- src/ssh_config/parser.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ssh_config/parser.rs b/src/ssh_config/parser.rs index 63674c0..d58a88e 100644 --- a/src/ssh_config/parser.rs +++ b/src/ssh_config/parser.rs @@ -4,7 +4,6 @@ use std::fs::File; use std::fs::OpenOptions; use std::io::{BufRead, BufReader, Write}; use std::path::Path; -use std::path::PathBuf; use std::str::FromStr; use crate::ssh;