Skip to content
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
15 changes: 11 additions & 4 deletions src/searchable.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use fuzzy_matcher::FuzzyMatcher;
use fuzzy_matcher::skim::SkimMatcherV2;
use fuzzy_matcher::FuzzyMatcher;

type SearchableFn<T> = dyn FnMut(&&T, &str) -> bool;

Expand Down Expand Up @@ -49,7 +49,10 @@ where
.iter()
.filter(|host| (self.filter)(host, value))
.map(|item| {
let score = self.matcher.fuzzy_match(item.search_text(), value).unwrap_or(0);
let score = self
.matcher
.fuzzy_match(item.search_text(), value)
.unwrap_or(0);
(item.clone(), score)
})
.collect();
Expand All @@ -72,13 +75,17 @@ where
self.filtered.is_empty()
}

pub fn non_filtered_iter(&self) -> std::slice::Iter<T> {
pub fn non_filtered_iter(&self) -> std::slice::Iter<'_, T> {
self.vec.iter()
}

pub fn iter(&self) -> std::slice::Iter<T> {
pub fn iter(&self) -> std::slice::Iter<'_, T> {
self.filtered.iter()
}

pub fn items(&self) -> Vec<T> {
self.vec.clone()
}
}

impl<'a, T> IntoIterator for &'a Searchable<T>
Expand Down
28 changes: 27 additions & 1 deletion src/ssh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ use serde::Serialize;
use std::collections::VecDeque;
use std::process::Command;

use crate::ssh_config::{self, parser_error::ParseError, HostVecExt};
use crate::searchable::SearchableItem;
use crate::ssh_config::{self, parser_error::ParseError, HostVecExt};

#[derive(Debug, Serialize, Clone)]
pub struct Host {
Expand Down Expand Up @@ -55,6 +55,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),
Expand Down
21 changes: 19 additions & 2 deletions src/ssh_config/parser.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use anyhow::Result;
use glob::glob;
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::str::FromStr;

use crate::ssh;

use super::host::Entry;
use super::parser_error::InvalidIncludeError;
use super::parser_error::InvalidIncludeErrorDetails;
Expand Down Expand Up @@ -57,6 +60,20 @@ impl Parser {
Ok(hosts)
}

pub fn save_into_file(hosts: Vec<ssh::Host>, path: &str) -> Result<()> {
let normalized_path = shellexpand::tilde(&path).to_string();
let path = std::fs::canonicalize(normalized_path)?;

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<Host>), ParseError> {
let mut parent_host = Host::new(Vec::new());
let mut hosts = Vec::new();
Expand Down
63 changes: 54 additions & 9 deletions src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,16 @@ 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},
window::{
delete::OnKeyPressData as DeletePopupWindowOnKeyPressData,
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 {
Expand All @@ -47,14 +54,15 @@ pub struct App {
search: Input,

table_state: TableState,
hosts: Searchable<ssh::Host>,
table_columns_constraints: Vec<Constraint>,

palette: tailwind::Palette,
pub(crate) hosts: Searchable<ssh::Host>,
pub(crate) palette: tailwind::Palette,
pub(crate) delete_popup_window: DeletePopupWindow,
}

#[derive(PartialEq)]
enum AppKeyAction {
pub enum AppKeyAction {
Ok,
Stop,
Continue,
Expand Down Expand Up @@ -116,6 +124,8 @@ impl App {
|| matcher.fuzzy_match(&host.aliases, search_value).is_some()
},
),

delete_popup_window: DeletePopupWindow::default(),
};
app.calculate_table_columns_constraints();

Expand Down Expand Up @@ -163,8 +173,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() {
Expand All @@ -190,6 +202,27 @@ impl App {
#[allow(clippy::enum_glob_use)]
use KeyCode::*;

// If Popup Window is active `consume` key events
if self.delete_popup_window.is_active() {
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(
self.config.sort_by_levenshtein,
on_key_press_data.hosts,
"",
|_, _| false,
);

return res;
}

let is_ctrl_pressed = key.modifiers.contains(KeyModifiers::CONTROL);

if is_ctrl_pressed {
Expand Down Expand Up @@ -243,6 +276,16 @@ impl App {
return Ok(AppKeyAction::Stop);
}
}
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(
self.hosts.items(),
host_to_delete_index,
host_to_delete,
));
}
_ => return Ok(AppKeyAction::Continue),
}

Expand Down Expand Up @@ -424,11 +467,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.delete_popup_window.is_active() {
app.delete_popup_window.render(f);
}

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;
Expand Down
Loading