use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use super::ctx::{Effectful, Effects, Nav};
use crate::app::{App, HostState, Screen, UiSelection};
pub(crate) fn editable_aliases(app: &App) -> Vec<String> {
app.hosts_state
.list()
.iter()
.filter(|h| h.source_file.is_none())
.map(|h| h.alias.clone())
.collect()
}
pub(crate) fn filtered_hosts(app: &App) -> Vec<(String, String)> {
filter_hosts(app.ui.tunnel_host_picker_query(), &app.hosts_state)
}
fn filter_hosts(query: &str, hosts: &HostState) -> Vec<(String, String)> {
let query = query.to_lowercase();
hosts
.list()
.iter()
.filter(|h| h.source_file.is_none())
.filter(|h| {
if query.is_empty() {
return true;
}
h.alias.to_lowercase().contains(&query) || h.hostname.to_lowercase().contains(&query)
})
.map(|h| (h.alias.clone(), h.hostname.clone()))
.collect()
}
struct TunnelHostPickerCtx<'a> {
ui: &'a mut UiSelection,
hosts: &'a HostState,
screen: &'a mut Screen,
effects: Effects,
}
impl Nav for TunnelHostPickerCtx<'_> {
fn screen_mut(&mut self) -> &mut Screen {
self.screen
}
}
impl Effectful for TunnelHostPickerCtx<'_> {
fn effects_mut(&mut self) -> &mut Effects {
&mut self.effects
}
}
impl TunnelHostPickerCtx<'_> {
fn filtered_hosts(&self) -> Vec<(String, String)> {
filter_hosts(self.ui.tunnel_host_picker_query(), self.hosts)
}
}
pub(super) fn handle_key(app: &mut App, key: KeyEvent) {
let effects = {
let mut ctx = TunnelHostPickerCtx {
ui: &mut app.ui,
hosts: &app.hosts_state,
screen: &mut app.screen,
effects: Effects::default(),
};
picker_key(&mut ctx, key);
ctx.effects
};
effects.apply(app);
}
fn picker_key(ctx: &mut TunnelHostPickerCtx, key: KeyEvent) {
let total = ctx.filtered_hosts().len();
match key.code {
KeyCode::Esc => close(ctx),
KeyCode::Down if total > 0 => {
let cur = ctx.ui.tunnel_host_picker_state().selected().unwrap_or(0);
let next = (cur + 1).min(total - 1);
ctx.ui.tunnel_host_picker_state_mut().select(Some(next));
}
KeyCode::Up => {
let cur = ctx.ui.tunnel_host_picker_state().selected().unwrap_or(0);
ctx.ui
.tunnel_host_picker_state_mut()
.select(Some(cur.saturating_sub(1)));
}
KeyCode::Enter => {
let Some(idx) = ctx.ui.tunnel_host_picker_state().selected() else {
return;
};
let Some((alias, _)) = ctx.filtered_hosts().into_iter().nth(idx) else {
return;
};
ctx.ui.tunnel_host_picker_state_mut().select(None);
ctx.ui.tunnel_host_picker_query_mut().clear();
ctx.defer(move |app| app.open_tunnel_add_form(alias));
}
KeyCode::Backspace => {
if ctx.ui.tunnel_host_picker_query().is_empty() {
close(ctx);
} else {
ctx.ui.tunnel_host_picker_query_mut().pop();
reset_cursor_after_query_change(ctx);
}
}
KeyCode::Char(c)
if !key.modifiers.contains(KeyModifiers::CONTROL)
&& ctx.ui.tunnel_host_picker_query().len() < 64 =>
{
ctx.ui.tunnel_host_picker_query_mut().push(c);
reset_cursor_after_query_change(ctx);
}
_ => {}
}
}
fn close(ctx: &mut TunnelHostPickerCtx) {
ctx.ui.tunnel_host_picker_state_mut().select(None);
ctx.ui.tunnel_host_picker_query_mut().clear();
ctx.set_screen(Screen::HostList);
}
fn reset_cursor_after_query_change(ctx: &mut TunnelHostPickerCtx) {
let total = ctx.filtered_hosts().len();
if total == 0 {
ctx.ui.tunnel_host_picker_state_mut().select(None);
} else {
ctx.ui.tunnel_host_picker_state_mut().select(Some(0));
}
}