use std::cmp::Ordering;
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Candidate {
pub alias: String,
pub hostname: String,
pub port: u16,
pub user: Option<String>,
pub tags: Vec<(String, String)>,
pub last_used_secs: Option<u64>,
pub source: Source,
pub delegated: Option<String>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Source {
SshConfig,
EsshConfig,
#[default]
Cache,
}
impl Source {
#[allow(dead_code)] pub fn label(&self) -> &'static str {
match self {
Source::SshConfig => "ssh_config",
Source::EsshConfig => "essh",
Source::Cache => "seen before",
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Match {
pub candidate: Candidate,
pub score: i64,
pub highlights: Vec<usize>,
pub matched_field: Field,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Field {
Alias,
Hostname,
User,
Tag,
}
impl Field {
pub fn label(&self) -> &'static str {
match self {
Field::Alias => "",
Field::Hostname => "hostname",
Field::User => "user",
Field::Tag => "tag",
}
}
}
fn score_subsequence(query: &str, text: &str) -> Option<(i64, Vec<usize>)> {
if query.is_empty() {
return Some((0, Vec::new()));
}
let q: Vec<char> = query.chars().flat_map(|c| c.to_lowercase()).collect();
let t: Vec<char> = text.chars().collect();
let tl: Vec<char> = text.chars().flat_map(|c| c.to_lowercase()).collect();
let mut score: i64 = 0;
let mut positions = Vec::with_capacity(q.len());
let mut ti = 0usize;
let mut last_hit: Option<usize> = None;
for &qc in &q {
let found = (ti..tl.len()).find(|&i| tl[i] == qc)?;
let at_boundary = found == 0
|| matches!(t[found - 1], '-' | '_' | '.' | ' ' | '/' | '=')
|| (t[found].is_uppercase() && !t[found - 1].is_uppercase());
if at_boundary {
score += 12;
}
if last_hit == Some(found.wrapping_sub(1)) {
score += 8;
}
if let Some(prev) = last_hit {
score -= ((found - prev - 1) as i64).min(6);
}
score += 1;
positions.push(found);
last_hit = Some(found);
ti = found + 1;
}
if tl.starts_with(&q) {
score += 25;
}
if tl.len() == q.len() {
score += 15;
}
score -= (tl.len() as i64 - q.len() as i64).min(20) / 4;
Some((score, positions))
}
fn recency_bonus(last_used_secs: Option<u64>) -> i64 {
match last_used_secs {
None => 0,
Some(s) if s < 300 => 10,
Some(s) if s < 3_600 => 7,
Some(s) if s < 86_400 => 5,
Some(s) if s < 604_800 => 2,
Some(_) => 1,
}
}
pub fn search(candidates: &[Candidate], query: &str) -> Vec<Match> {
let q = query.trim();
let mut out: Vec<Match> = candidates
.iter()
.filter_map(|c| {
if q.is_empty() {
return Some(Match {
candidate: c.clone(),
score: recency_bonus(c.last_used_secs),
highlights: Vec::new(),
matched_field: Field::Alias,
});
}
let mut best: Option<(i64, Vec<usize>, Field)> = None;
let mut consider = |s: Option<(i64, Vec<usize>)>, weight: i64, field: Field| {
if let Some((sc, pos)) = s {
let total = sc + weight;
if best.as_ref().map(|(b, _, _)| total > *b).unwrap_or(true) {
best = Some((total, pos, field));
}
}
};
consider(score_subsequence(q, &c.alias), 30, Field::Alias);
consider(score_subsequence(q, &c.hostname), 0, Field::Hostname);
if let Some(u) = &c.user {
consider(score_subsequence(q, u), -10, Field::User);
}
for (k, v) in &c.tags {
consider(
score_subsequence(q, &format!("{}={}", k, v)),
-5,
Field::Tag,
);
}
let (score, highlights, matched_field) = best?;
Some(Match {
candidate: c.clone(),
score: score + recency_bonus(c.last_used_secs),
highlights: if matched_field == Field::Alias {
highlights
} else {
Vec::new()
},
matched_field,
})
})
.collect();
out.sort_by(|a, b| {
b.score
.cmp(&a.score)
.then_with(
|| match (a.candidate.last_used_secs, b.candidate.last_used_secs) {
(Some(x), Some(y)) => x.cmp(&y),
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => Ordering::Equal,
},
)
.then_with(|| a.candidate.alias.cmp(&b.candidate.alias))
});
out
}
#[cfg(test)]
mod tests {
use super::*;
fn c(alias: &str, hostname: &str) -> Candidate {
Candidate {
alias: alias.into(),
hostname: hostname.into(),
port: 22,
..Default::default()
}
}
fn fleet() -> Vec<Candidate> {
vec![
c("prod-db", "10.0.0.5"),
c("prod-api-01", "10.0.1.10"),
c("prod-api-02", "10.0.1.11"),
c("staging-db", "10.1.0.5"),
c("bastion", "bastion.example.com"),
c("parallels-host", "192.168.64.2"),
]
}
fn top(query: &str) -> String {
search(&fleet(), query)
.first()
.map(|m| m.candidate.alias.clone())
.unwrap_or_default()
}
#[test]
fn initials_find_the_host_they_obviously_mean() {
assert_eq!(top("pdb"), "prod-db");
assert_eq!(top("sdb"), "staging-db");
assert_eq!(top("bas"), "bastion");
}
#[test]
fn a_contiguous_prefix_beats_scattered_initials() {
assert_eq!(top("pa"), "parallels-host");
}
#[test]
fn word_starts_beat_matches_buried_mid_word() {
let cands = vec![
c("prod-api-01", "10.0.1.10"),
c("capacity-node", "10.5.0.1"),
];
let hits = search(&cands, "pa");
assert_eq!(hits[0].candidate.alias, "prod-api-01");
}
#[test]
fn an_exact_alias_outranks_a_longer_prefix_match() {
let cands = vec![c("db", "10.0.0.1"), c("db-replica-01", "10.0.0.2")];
let hits = search(&cands, "db");
assert_eq!(hits[0].candidate.alias, "db");
}
#[test]
fn a_non_matching_query_returns_nothing_rather_than_everything() {
assert!(search(&fleet(), "zzzzz").is_empty());
}
#[test]
fn an_empty_query_lists_everything_most_recent_first() {
let mut cands = fleet();
cands[3].last_used_secs = Some(60); cands[0].last_used_secs = Some(86_400 * 30); let hits = search(&cands, "");
assert_eq!(hits.len(), cands.len());
assert_eq!(hits[0].candidate.alias, "staging-db");
}
#[test]
fn recency_breaks_ties_but_never_overrides_a_better_match() {
let mut cands = vec![
c("prod-db", "10.0.0.5"),
c("parallels-host", "192.168.64.2"),
];
cands[1].last_used_secs = Some(1);
let hits = search(&cands, "pdb");
assert_eq!(
hits[0].candidate.alias, "prod-db",
"recency must not beat a clearly better match"
);
}
#[test]
fn recency_does_decide_between_equal_matches() {
let mut a = c("web-01", "10.0.0.1");
let mut b = c("web-02", "10.0.0.2");
a.last_used_secs = Some(86_400 * 30);
b.last_used_secs = Some(60);
let hits = search(&[a, b], "web");
assert_eq!(hits[0].candidate.alias, "web-02");
}
#[test]
fn hostname_and_tags_are_searchable_not_just_the_alias() {
let mut cand = c("mystery", "prod-db-7.internal");
cand.tags = vec![("role".into(), "database".into())];
let cands = vec![cand, c("unrelated", "10.9.9.9")];
let by_hostname = search(&cands, "internal");
assert_eq!(by_hostname[0].candidate.alias, "mystery");
assert_eq!(by_hostname[0].matched_field, Field::Hostname);
let by_tag = search(&cands, "role=data");
assert_eq!(by_tag[0].candidate.alias, "mystery");
assert_eq!(by_tag[0].matched_field, Field::Tag);
}
#[test]
fn the_alias_wins_when_several_fields_match() {
let mut cand = c("db", "db.example.com");
cand.user = Some("db".into());
let hits = search(&[cand], "db");
assert_eq!(hits[0].matched_field, Field::Alias);
}
#[test]
fn highlights_point_at_the_characters_that_matched() {
let hits = search(&[c("prod-db", "10.0.0.5")], "pdb");
let m = &hits[0];
assert_eq!(m.matched_field, Field::Alias);
let chars: Vec<char> = m.candidate.alias.chars().collect();
let matched: String = m.highlights.iter().map(|&i| chars[i]).collect();
assert_eq!(matched, "pdb");
}
#[test]
fn results_are_stable_across_identical_searches() {
let cands = fleet();
let first: Vec<String> = search(&cands, "p")
.iter()
.map(|m| m.candidate.alias.clone())
.collect();
for _ in 0..20 {
let again: Vec<String> = search(&cands, "p")
.iter()
.map(|m| m.candidate.alias.clone())
.collect();
assert_eq!(first, again);
}
}
#[test]
fn matching_is_case_insensitive_both_ways() {
let cands = vec![c("Prod-DB", "10.0.0.5")];
assert!(!search(&cands, "prod").is_empty());
assert!(!search(&cands, "PROD").is_empty());
}
#[test]
fn source_is_reported_so_config_hosts_are_distinguishable() {
let mut a = c("from-ssh", "10.0.0.1");
a.source = Source::SshConfig;
let hits = search(&[a], "");
assert_eq!(hits[0].candidate.source.label(), "ssh_config");
}
}
pub fn collect_candidates(
ssh_config: Option<&crate::sshconfig::SshConfig>,
essh_hosts: &[crate::config::HostEntry],
cached: &[crate::cache::CachedHost],
local_user: &str,
now: chrono::DateTime<chrono::Utc>,
) -> Vec<Candidate> {
let mut out: Vec<Candidate> = Vec::new();
fn upsert(out: &mut Vec<Candidate>, cand: Candidate) {
if let Some(existing) = out.iter_mut().find(|c| c.alias == cand.alias) {
if existing.user.is_none() {
existing.user = cand.user;
}
if existing.tags.is_empty() {
existing.tags = cand.tags;
}
if existing.last_used_secs.is_none() {
existing.last_used_secs = cand.last_used_secs;
}
} else {
out.push(cand);
}
}
for h in essh_hosts {
upsert(
&mut out,
Candidate {
alias: h.name.clone(),
hostname: h.hostname.clone(),
port: h.port,
user: h.user.clone(),
tags: crate::format::sorted_tags(&h.tags),
last_used_secs: None,
source: Source::EsshConfig,
delegated: None,
},
);
}
if let Some(cfg) = ssh_config {
for alias in cfg.aliases() {
let r = cfg.resolve(&alias, local_user);
upsert(
&mut out,
Candidate {
alias,
hostname: r.hostname.clone(),
port: r.port,
user: r.user.clone(),
tags: Vec::new(),
last_used_secs: None,
source: Source::SshConfig,
delegated: r.caveat_summary(),
},
);
}
}
for h in cached {
let age = chrono::DateTime::parse_from_rfc3339(&h.last_seen)
.ok()
.map(|t| (now - t.with_timezone(&chrono::Utc)).num_seconds().max(0) as u64);
if let Some(existing) = out
.iter_mut()
.find(|c| c.hostname == h.hostname && c.port == h.port)
{
existing.last_used_secs = age;
if existing.tags.is_empty() {
existing.tags = crate::format::sorted_tags(&h.tags);
}
continue;
}
upsert(
&mut out,
Candidate {
alias: h.hostname.clone(),
hostname: h.hostname.clone(),
port: h.port,
user: None,
tags: crate::format::sorted_tags(&h.tags),
last_used_secs: age,
source: Source::Cache,
delegated: None,
},
);
}
out
}
#[cfg(test)]
mod collect_tests {
use super::*;
use crate::config::HostEntry;
use std::collections::HashMap;
fn essh_host(name: &str, hostname: &str, port: u16) -> HostEntry {
HostEntry {
name: name.into(),
hostname: hostname.into(),
port,
user: Some("deploy".into()),
key: None,
tags: HashMap::from([("role".to_string(), "web".to_string())]),
jump_host: None,
port_forwards: Vec::new(),
}
}
fn cached(hostname: &str, port: u16, last_seen: &str) -> crate::cache::CachedHost {
crate::cache::CachedHost {
id: 1,
hostname: hostname.into(),
ip: None,
port,
fingerprint: "SHA256:x".into(),
key_type: "ssh-ed25519".into(),
first_seen: last_seen.into(),
last_seen: last_seen.into(),
tags: HashMap::new(),
}
}
fn now() -> chrono::DateTime<chrono::Utc> {
chrono::DateTime::parse_from_rfc3339("2026-08-15T12:00:00Z")
.unwrap()
.with_timezone(&chrono::Utc)
}
#[test]
fn ssh_config_hosts_appear_without_an_import_step() {
let cfg = crate::sshconfig::SshConfig::parse_str(
"Host prod-db\n HostName 10.0.0.5\n Port 2222\n User deploy\n",
);
let cands = collect_candidates(Some(&cfg), &[], &[], "matt", now());
assert_eq!(cands.len(), 1);
assert_eq!(cands[0].alias, "prod-db");
assert_eq!(cands[0].hostname, "10.0.0.5");
assert_eq!(cands[0].port, 2222);
assert_eq!(cands[0].source, Source::SshConfig);
}
#[test]
fn a_host_in_both_configs_appears_once_not_twice() {
let cfg = crate::sshconfig::SshConfig::parse_str("Host prod-db\n HostName 10.9.9.9\n");
let essh = vec![essh_host("prod-db", "10.0.0.5", 22)];
let cands = collect_candidates(Some(&cfg), &essh, &[], "matt", now());
assert_eq!(cands.len(), 1);
assert_eq!(cands[0].hostname, "10.0.0.5");
assert_eq!(cands[0].source, Source::EsshConfig);
}
#[test]
fn the_cache_contributes_recency_to_a_configured_host() {
let essh = vec![essh_host("prod-db", "10.0.0.5", 22)];
let seen = vec![cached("10.0.0.5", 22, "2026-08-15T11:30:00Z")];
let cands = collect_candidates(None, &essh, &seen, "matt", now());
assert_eq!(cands.len(), 1, "the cache must not add a duplicate row");
assert_eq!(cands[0].last_used_secs, Some(1800));
assert_eq!(cands[0].source, Source::EsshConfig);
}
#[test]
fn a_host_only_in_the_cache_is_still_offered() {
let seen = vec![cached("10.4.4.4", 22, "2026-08-15T11:00:00Z")];
let cands = collect_candidates(None, &[], &seen, "matt", now());
assert_eq!(cands.len(), 1);
assert_eq!(cands[0].source, Source::Cache);
assert_eq!(cands[0].last_used_secs, Some(3600));
}
#[test]
fn wildcard_blocks_do_not_become_launcher_entries() {
let cfg = crate::sshconfig::SshConfig::parse_str(
"Host *\n User matt\n\nHost real\n HostName 10.0.0.1\n",
);
let cands = collect_candidates(Some(&cfg), &[], &[], "matt", now());
assert_eq!(cands.len(), 1);
assert_eq!(cands[0].alias, "real");
}
#[test]
fn a_future_last_seen_does_not_underflow_the_age() {
let seen = vec![cached("10.4.4.4", 22, "2026-08-15T13:00:00Z")];
let cands = collect_candidates(None, &[], &seen, "matt", now());
assert_eq!(cands[0].last_used_secs, Some(0));
}
#[test]
fn everything_is_searchable_once_collected() {
let cfg =
crate::sshconfig::SshConfig::parse_str("Host bastion\n HostName b.example.com\n");
let essh = vec![essh_host("prod-db", "10.0.0.5", 22)];
let cands = collect_candidates(Some(&cfg), &essh, &[], "matt", now());
assert_eq!(search(&cands, "bas")[0].candidate.alias, "bastion");
assert_eq!(search(&cands, "pdb")[0].candidate.alias, "prod-db");
assert_eq!(search(&cands, "role=web")[0].candidate.alias, "prod-db");
}
}