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
5 changes: 5 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ struct Args {
#[arg(long, default_value_t = true)]
sort: bool,

/// Fancy Levenshtein sort by edit distance
#[arg(long, default_value_t = false)]
sort_fancy: bool,

/// Handlebars template of the command to execute
#[arg(short, long, default_value = "ssh \"{{{name}}}\"")]
template: String,
Expand All @@ -58,6 +62,7 @@ fn main() -> Result<()> {
config_paths: args.config,
search_filter: args.search,
sort_by_name: args.sort,
sort_by_levenshtein: args.sort_fancy,
show_proxy_command: args.show_proxy_command,
command_template: args.template,
command_template_on_session_start: args.on_session_start_template,
Expand Down
45 changes: 36 additions & 9 deletions src/searchable.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,37 @@
use fuzzy_matcher::FuzzyMatcher;
use fuzzy_matcher::skim::SkimMatcherV2;

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

pub trait SearchableItem {
fn search_text(&self) -> &str;
fn from_search_value(value: &str) -> Self;
}

pub struct Searchable<T>
where
T: Clone,
T: Clone + SearchableItem,
{
sort_by_levenshtein: bool,
vec: Vec<T>,

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

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

matcher: SkimMatcherV2::default(),
filter: Box::new(predicate),
filtered: Vec::new(),
};
Expand All @@ -35,12 +45,29 @@ where
return;
}

self.filtered = self
let mut items: Vec<_> = self
.vec
.iter()
.filter(|host| (self.filter)(host, value))
.cloned()
.map(|item| {
let score = self.matcher.fuzzy_match(item.search_text(), value).unwrap_or(0);
(item.clone(), score)
})
.collect();

// Sort by Levenshtein distance in descending order (higher score = better match)
if self.sort_by_levenshtein {
items.sort_by(|a, b| b.1.cmp(&a.1));
}

// If no results found, return the search value itself
if items.is_empty() {
// Create a dummy item with the search value
let dummy_item = T::from_search_value(value);
self.filtered = vec![dummy_item];
} else {
self.filtered = items.into_iter().map(|(item, _)| item).collect();
}
}

#[allow(clippy::must_use_candidate)]
Expand All @@ -64,7 +91,7 @@ where

impl<'a, T> IntoIterator for &'a Searchable<T>
where
T: Clone,
T: Clone + SearchableItem,
{
type Item = &'a T;
type IntoIter = std::slice::Iter<'a, T>;
Expand All @@ -76,7 +103,7 @@ where

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

Expand Down
18 changes: 18 additions & 0 deletions src/ssh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::collections::VecDeque;
use std::process::Command;

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

#[derive(Debug, Serialize, Clone)]
pub struct Host {
Expand All @@ -17,6 +18,23 @@ pub struct Host {
pub proxy_command: Option<String>,
}

impl SearchableItem for Host {
fn search_text(&self) -> &str {
&self.name
}

fn from_search_value(value: &str) -> Self {
Host {
name: value.to_string(),
aliases: String::new(),
user: None,
destination: String::new(),
port: None,
proxy_command: None,
}
}
}

impl Host {
/// Uses the provided Handlebars template to run a command.
///
Expand Down
2 changes: 2 additions & 0 deletions src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub struct AppConfig {

pub search_filter: Option<String>,
pub sort_by_name: bool,
pub sort_by_levenshtein: bool,
pub show_proxy_command: bool,

pub command_template: String,
Expand Down Expand Up @@ -103,6 +104,7 @@ impl App {
palette: tailwind::BLUE,

hosts: Searchable::new(
config.sort_by_levenshtein,
hosts,
&search_input,
move |host: &&ssh::Host, search_value: &str| -> bool {
Expand Down