Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
4 changes: 4 additions & 0 deletions src/searchable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ where
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
26 changes: 26 additions & 0 deletions src/ssh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
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
60 changes: 50 additions & 10 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 @@ -46,14 +53,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 @@ -103,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()
Expand All @@ -114,6 +122,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 @@ -161,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() {
Expand All @@ -188,6 +200,22 @@ 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(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 @@ -241,6 +269,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 @@ -422,11 +460,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