Skip to content

Commit

Permalink
fix: don't select hosts outside of filtered hosts
Browse files Browse the repository at this point in the history
Signed-off-by: Nathanael DEMACON <[email protected]>
  • Loading branch information
quantumsheep committed Feb 21, 2024
1 parent 22d9479 commit b0460e6
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 29 deletions.
7 changes: 6 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod searchable;
pub mod ssh;
pub mod ssh_config;
pub mod ui;
Expand Down Expand Up @@ -26,7 +27,11 @@ struct Args {
sort: bool,

/// Handlebars template of the command to execute
#[arg(short, long, default_value = "ssh {{#if user}}{{user}}@{{/if}}{{destination}}{{#if port}} -p {{port}}{{/if}}")]
#[arg(
short,
long,
default_value = "ssh {{#if user}}{{user}}@{{/if}}{{destination}}{{#if port}} -p {{port}}{{/if}}"
)]
template: String,

/// Exit after ending the SSH session
Expand Down
82 changes: 82 additions & 0 deletions src/searchable.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
type SearchableFn<T> = dyn FnMut(&&T, &str) -> bool;

pub struct Searchable<T>
where
T: Clone,
{
vec: Vec<T>,

filter: Box<SearchableFn<T>>,
filtered: Vec<T>,
}

impl<T> Searchable<T>
where
T: Clone,
{
#[must_use]
pub fn new<P>(vec: Vec<T>, search_value: &str, predicate: P) -> Self
where
P: FnMut(&&T, &str) -> bool + 'static,
{
let mut searchable = Self {
vec,

filter: Box::new(predicate),
filtered: Vec::new(),
};
searchable.search(search_value);
searchable
}

pub fn search(&mut self, value: &str) {
if value.is_empty() {
self.filtered = self.vec.clone();
return;
}

self.filtered = self
.vec
.iter()
.filter(|host| (self.filter)(host, value))
.cloned()
.collect();
}

#[allow(clippy::must_use_candidate)]
pub fn len(&self) -> usize {
self.filtered.len()
}

#[allow(clippy::must_use_candidate)]
pub fn is_empty(&self) -> bool {
self.filtered.is_empty()
}

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

impl<'a, T> IntoIterator for &'a Searchable<T>
where
T: Clone,
{
type Item = &'a T;
type IntoIter = std::slice::Iter<'a, T>;

fn into_iter(self) -> Self::IntoIter {
self.filtered.iter()
}
}

impl<T> std::ops::Index<usize> for Searchable<T>
where
T: Clone,
{
type Output = T;

fn index(&self, index: usize) -> &Self::Output {
&self.filtered[index]
}
}
63 changes: 35 additions & 28 deletions src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use tui_input::backend::crossterm::EventHandler;
use tui_input::Input;
use unicode_width::UnicodeWidthStr;

use crate::ssh;
use crate::{searchable::Searchable, ssh};

const INFO_TEXT: &str = "(Esc) quit | (↑) move up | (↓) move down | (enter) select";

Expand All @@ -40,7 +40,7 @@ pub struct App {
search: Input,

table_state: TableState,
hosts: Vec<ssh::Host>,
hosts: Searchable<ssh::Host>,
table_longest_item_lens: (u16, u16, u16, u16, u16),

palette: tailwind::Palette,
Expand All @@ -58,16 +58,26 @@ impl App {

let search_input = config.search_filter.clone().unwrap_or_default();

let matcher = SkimMatcherV2::default();

Ok(App {
config: config.clone(),

search: search_input.into(),
search: search_input.clone().into(),

table_state: TableState::default().with_selected(0),
table_longest_item_lens: constraint_len_calculator(&hosts),
palette: tailwind::BLUE,

hosts,
hosts: Searchable::new(
hosts,
&search_input,
move |host: &&ssh::Host, search_value: &str| -> bool {
search_value.is_empty()
|| matcher.fuzzy_match(&host.name, search_value).is_some()
|| matcher.fuzzy_match(&host.aliases, search_value).is_some()
},
),
})
}

Expand Down Expand Up @@ -140,6 +150,15 @@ impl App {
}
_ => {
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() {
self.table_state.select(Some(match self.hosts.len() {
0 => 0,
_ => self.hosts.len() - 1,
}));
}
}
}
}
Expand Down Expand Up @@ -258,31 +277,19 @@ fn render_table(f: &mut Frame, app: &mut App, area: Rect) {
.style(header_style)
.height(1);

let search_value = app.search.value();

let matcher = SkimMatcherV2::default();

let rows = app
.hosts
let rows = app.hosts.iter().map(|host| {
[
&host.name,
&host.aliases,
&host.user.as_ref().unwrap_or(&String::new()),
&host.destination,
&host.port.as_ref().unwrap_or(&String::new()),
]
.iter()
.filter(|host| {
search_value.is_empty()
|| matcher.fuzzy_match(&host.name, search_value).is_some()
|| matcher.fuzzy_match(&host.aliases, search_value).is_some()
})
.map(|host| {
[
&host.name,
&host.aliases,
&host.user.as_ref().unwrap_or(&String::new()),
&host.destination,
&host.port.as_ref().unwrap_or(&String::new()),
]
.iter()
.copied()
.map(|content| Cell::from(Text::from(content.to_string())))
.collect::<Row>()
});
.copied()
.map(|content| Cell::from(Text::from(content.to_string())))
.collect::<Row>()
});

let bar = " █ ";
let t = Table::new(
Expand Down

0 comments on commit b0460e6

Please sign in to comment.