use crossterm::event::{KeyCode, KeyEvent};
use log::{debug, info};
use crate::app::{App, Screen, TopPage};
fn tunnel_form_return_screen(app: &App, alias: &str) -> Screen {
if matches!(app.top_page, TopPage::Tunnels) {
Screen::HostList
} else {
Screen::TunnelList {
alias: alias.to_string(),
}
}
}
pub(super) fn handle_tunnel_list_key(app: &mut App, key: KeyEvent) {
let alias = match &app.screen {
Screen::TunnelList { alias } => alias.clone(),
_ => return,
};
if app.tunnels.pending_delete().is_some() && key.code != KeyCode::Char('?') {
match super::route_confirm_key(key) {
super::ConfirmAction::Yes => {
let Some(sel) = app.tunnels.take_pending_delete() else {
return;
};
if let Some(rule) = app.tunnels.list().get(sel) {
let key = rule.tunnel_type.directive_key().to_string();
let value = rule.to_directive_value();
let config_backup = app.hosts_state.ssh_config().clone();
if !app
.hosts_state
.ssh_config_mut()
.remove_forward(&alias, &key, &value)
{
app.notify_warning(crate::messages::TUNNEL_NOT_FOUND);
return;
}
if let Err(e) = app.hosts_state.ssh_config().write() {
app.hosts_state.set_ssh_config(config_backup);
app.notify_error(crate::messages::failed_to_save(&e));
} else {
app.update_last_modified();
app.refresh_tunnel_list(&alias);
app.reload_hosts();
if app.tunnels.list().is_empty() {
app.ui.tunnel_list_state_mut().select(None);
} else if sel >= app.tunnels.list().len() {
app.ui
.tunnel_list_state_mut()
.select(Some(app.tunnels.list().len() - 1));
}
app.notify(crate::messages::TUNNEL_REMOVED);
}
}
}
super::ConfirmAction::No => {
app.tunnels.cancel_delete();
}
super::ConfirmAction::Ignored => {}
}
return;
}
match key.code {
KeyCode::Esc | KeyCode::Char('q') => {
app.set_screen(Screen::HostList);
}
KeyCode::Char('j') | KeyCode::Down => {
app.select_next_tunnel();
}
KeyCode::Char('k') | KeyCode::Up => {
app.select_prev_tunnel();
}
KeyCode::PageDown => {
crate::app::page_down(app.ui.tunnel_list_state_mut(), app.tunnels.list().len(), 10);
}
KeyCode::PageUp => {
crate::app::page_up(app.ui.tunnel_list_state_mut(), app.tunnels.list().len(), 10);
}
KeyCode::Char('a') => {
if let Some(host) = app.hosts_state.list().iter().find(|h| h.alias == alias) {
if host.source_file.is_some() {
app.notify_warning(crate::messages::TUNNEL_INCLUDED_READ_ONLY);
return;
}
}
app.open_tunnel_add_form(alias.clone());
}
KeyCode::Char('e') => {
if let Some(host) = app.hosts_state.list().iter().find(|h| h.alias == alias) {
if host.source_file.is_some() {
app.notify_warning(crate::messages::TUNNEL_INCLUDED_READ_ONLY);
return;
}
}
if let Some(sel) = app.ui.tunnel_list_state().selected() {
if let Some(rule) = app.tunnels.list().get(sel).cloned() {
app.open_tunnel_edit_form(alias.clone(), &rule, sel);
}
}
}
KeyCode::Char('d') => {
if let Some(host) = app.hosts_state.list().iter().find(|h| h.alias == alias) {
if host.source_file.is_some() {
app.notify_warning(crate::messages::TUNNEL_INCLUDED_READ_ONLY);
return;
}
}
if let Some(sel) = app.ui.tunnel_list_state().selected() {
if sel < app.tunnels.list().len() {
app.tunnels.request_delete(sel);
}
}
}
KeyCode::Enter => {
if app.tunnels.active_contains(&alias) {
if let Some(mut tunnel) = app.tunnels.active_remove(&alias) {
if let Err(e) = tunnel.child.kill() {
debug!("[external] Failed to kill tunnel process for {alias}: {e}");
}
let _ = tunnel.child.wait();
drop(tunnel);
app.refresh_tunnel_bind_ports();
app.notify(crate::messages::tunnel_stopped(&alias));
}
} else if !app.tunnels.list().is_empty() {
if app.demo_mode {
app.notify_warning(crate::messages::DEMO_TUNNELS_DISABLED);
return;
}
let askpass = app
.hosts_state
.list()
.iter()
.find(|h| h.alias == alias)
.and_then(|h| h.askpass.clone());
match crate::tunnel::start_tunnel(
&alias,
app.reload.config_path(),
askpass.as_deref(),
app.bw_session.as_deref(),
) {
Ok(child) => {
for rule in app.tunnels.list() {
info!(
"Tunnel started: type={} local={} remote={}:{} alias={alias}",
rule.tunnel_type.label(),
rule.bind_port,
rule.remote_host,
rule.remote_port
);
}
app.tunnels.ensure_lsof_poller();
let parser_tx = app.tunnels.parser_tx();
let active = crate::tunnel::ActiveTunnel::spawn(child, &alias, parser_tx);
app.tunnels.active_insert(alias.clone(), active);
app.refresh_tunnel_bind_ports();
app.history.record(&alias);
app.record_key_use(&alias, crate::key_activity::now_secs());
app.apply_sort();
app.notify(crate::messages::tunnel_started(&alias));
}
Err(e) => {
log::error!("[external] tunnel start failed: alias={alias}: {e}");
app.notify_error(crate::messages::tunnel_start_failed(&e));
}
}
}
}
KeyCode::Char('?') => {
let old = std::mem::replace(&mut app.screen, Screen::HostList);
app.set_screen(Screen::Help {
return_screen: Box::new(old),
});
}
_ => {}
}
}
pub(super) fn handle_tunnel_form_key(app: &mut App, key: KeyEvent) {
let (alias, editing) = match &app.screen {
Screen::TunnelForm { alias, editing } => (alias.clone(), *editing),
_ => return,
};
if app.forms.is_discard_pending() {
match super::route_confirm_key(key) {
super::ConfirmAction::Yes => {
app.forms.dismiss_discard_confirm();
let return_to = tunnel_form_return_screen(app, &alias);
app.close_tunnel_form(return_to);
}
super::ConfirmAction::No => {
app.forms.dismiss_discard_confirm();
}
super::ConfirmAction::Ignored => {}
}
return;
}
match key.code {
KeyCode::Esc => {
if app.tunnel_form_is_dirty() {
app.forms.request_discard_confirm();
} else {
let return_to = tunnel_form_return_screen(app, &alias);
app.close_tunnel_form(return_to);
}
}
KeyCode::Tab | KeyCode::Down => {
app.tunnels.form_mut().focus_next();
}
KeyCode::BackTab | KeyCode::Up => {
app.tunnels.form_mut().focus_prev();
}
KeyCode::Left if app.tunnels.form_mut().cursor_pos > 0 => {
app.tunnels.form_mut().cursor_pos -= 1;
}
KeyCode::Right => {
let len = app
.tunnels
.form()
.focused_value()
.map(|v| v.chars().count())
.unwrap_or(0);
if app.tunnels.form_mut().cursor_pos < len {
app.tunnels.form_mut().cursor_pos += 1;
}
}
KeyCode::Home => {
app.tunnels.form_mut().cursor_pos = 0;
}
KeyCode::End => {
app.tunnels.form_mut().sync_cursor_to_end();
}
KeyCode::Enter => {
submit_tunnel_form(app, &alias, editing);
}
KeyCode::Char(' ')
if app.tunnels.form_mut().focused_field == crate::app::TunnelFormField::Type =>
{
app.tunnels.form_mut().tunnel_type = app.tunnels.form_mut().tunnel_type.next();
}
KeyCode::Char(c) => {
app.tunnels.form_mut().insert_char(c);
}
KeyCode::Backspace => {
app.tunnels.form_mut().delete_char_before_cursor();
}
_ => {}
}
}
fn submit_tunnel_form(app: &mut App, alias: &str, editing: Option<usize>) {
if app.config_changed_since_form_open() {
app.notify_warning(crate::messages::CONFIG_CHANGED_EXTERNALLY);
return;
}
if let Err(msg) = app.tunnels.form_mut().validate() {
app.notify_error(msg);
return;
}
let (directive_key, directive_value) = app.tunnels.form_mut().to_directive();
let config_backup = app.hosts_state.ssh_config().clone();
if let Some(idx) = editing {
if let Some(old_rule) = app.tunnels.list().get(idx) {
let old_key = old_rule.tunnel_type.directive_key().to_string();
let old_value = old_rule.to_directive_value();
if !app
.hosts_state
.ssh_config_mut()
.remove_forward(alias, &old_key, &old_value)
{
app.hosts_state.set_ssh_config(config_backup);
app.notify_warning(crate::messages::TUNNEL_ORIGINAL_NOT_FOUND);
return;
}
} else {
app.notify_warning(crate::messages::TUNNEL_LIST_CHANGED);
return;
}
}
if app
.hosts_state
.ssh_config()
.has_forward(alias, directive_key, &directive_value)
{
app.hosts_state.set_ssh_config(config_backup);
app.notify_warning(crate::messages::TUNNEL_DUPLICATE);
return;
}
app.hosts_state
.ssh_config_mut()
.add_forward(alias, directive_key, &directive_value);
if let Err(e) = app.hosts_state.ssh_config().write() {
app.hosts_state.set_ssh_config(config_backup);
app.notify_error(crate::messages::failed_to_save(&e));
return;
}
app.hosts_state.clear_undo(); app.update_last_modified();
app.refresh_tunnel_list(alias);
app.reload_hosts();
if app.tunnels.list().is_empty() {
app.ui.tunnel_list_state_mut().select(None);
} else if let Some(sel) = app.ui.tunnel_list_state().selected() {
if sel >= app.tunnels.list().len() {
app.ui
.tunnel_list_state_mut()
.select(Some(app.tunnels.list().len() - 1));
}
} else {
app.ui.tunnel_list_state_mut().select(Some(0));
}
app.notify(crate::messages::TUNNEL_SAVED);
let return_to = tunnel_form_return_screen(app, alias);
app.close_tunnel_form(return_to);
}