diff --git a/src/config.rs b/src/config.rs index 56fe11de6..d60767df0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,6 @@ use anyhow::anyhow; use clap::{Parser, ValueEnum}; +use crossterm::event::{KeyCode, KeyModifiers}; use std::collections::HashMap; use std::net::IpAddr; use std::process; @@ -276,8 +277,16 @@ pub struct Args { #[clap(long, display_order = 32)] pub print_tui_theme_items: bool, + /// The TUI key bindings [command=key,command=key,..] + #[clap(long, value_delimiter(','), value_parser = parse_tui_binding_value, display_order = 33)] + pub tui_key_bindings: Vec<(TuiCommandItem, TuiKeyBinding)>, + + /// Print all TUI commands that can be bound and exit + #[clap(long, display_order = 34)] + pub print_tui_binding_commands: bool, + /// The number of report cycles to run - #[clap(short = 'c', long, default_value_t = 10, display_order = 33)] + #[clap(short = 'c', long, default_value_t = 10, display_order = 35)] pub report_cycles: usize, } @@ -290,6 +299,15 @@ fn parse_tui_theme_color_value(value: &str) -> anyhow::Result<(TuiThemeItem, Tui Ok((item, color)) } +fn parse_tui_binding_value(value: &str) -> anyhow::Result<(TuiCommandItem, TuiKeyBinding)> { + let pos = value + .find('=') + .ok_or_else(|| anyhow!("invalid binding value: expected format `item=value`"))?; + let item = TuiCommandItem::try_from(&value[..pos])?; + let binding = TuiKeyBinding::try_from(&value[pos + 1..])?; + Ok((item, binding)) +} + /// Fully parsed and validate configuration. pub struct TrippyConfig { pub targets: Vec, @@ -319,6 +337,7 @@ pub struct TrippyConfig { pub tui_address_mode: AddressMode, pub tui_max_addrs: Option, pub tui_theme: TuiTheme, + pub tui_bindings: TuiBindings, pub mode: Mode, pub report_cycles: usize, pub max_rounds: Option, @@ -517,6 +536,314 @@ impl TryFrom<&str> for TuiColor { } } +/// Tui keyboard bindings. +#[derive(Debug, Clone, Copy)] +pub struct TuiBindings { + pub toggle_help: TuiKeyBinding, + pub up: TuiKeyBinding, + pub down: TuiKeyBinding, + pub left: TuiKeyBinding, + pub right: TuiKeyBinding, + pub address_mode_ip: TuiKeyBinding, + pub address_mode_host: TuiKeyBinding, + pub address_mode_both: TuiKeyBinding, + pub toggle_freeze: TuiKeyBinding, + pub toggle_chart: TuiKeyBinding, + pub expand_hosts: TuiKeyBinding, + pub contract_hosts: TuiKeyBinding, + pub expand_hosts_max: TuiKeyBinding, + pub contract_hosts_min: TuiKeyBinding, + pub chart_zoom_in: TuiKeyBinding, + pub chart_zoom_out: TuiKeyBinding, + pub clear_trace_data: TuiKeyBinding, + pub clear_dns_cache: TuiKeyBinding, + pub clear_selection: TuiKeyBinding, + pub toggle_as_info: TuiKeyBinding, + pub quit: TuiKeyBinding, +} + +impl From> for TuiBindings { + fn from(value: HashMap) -> Self { + Self { + toggle_help: *value + .get(&TuiCommandItem::ToggleHelp) + .unwrap_or(&TuiKeyBinding::new(KeyCode::Char('h'))), + up: *value + .get(&TuiCommandItem::PreviousHop) + .unwrap_or(&TuiKeyBinding::new(KeyCode::Up)), + down: *value + .get(&TuiCommandItem::NextHop) + .unwrap_or(&TuiKeyBinding::new(KeyCode::Down)), + left: *value + .get(&TuiCommandItem::PreviousTrace) + .unwrap_or(&TuiKeyBinding::new(KeyCode::Left)), + right: *value + .get(&TuiCommandItem::NextTrace) + .unwrap_or(&TuiKeyBinding::new(KeyCode::Right)), + address_mode_ip: *value + .get(&TuiCommandItem::AddressModeIp) + .unwrap_or(&TuiKeyBinding::new(KeyCode::Char('i'))), + address_mode_host: *value + .get(&TuiCommandItem::AddressModeHost) + .unwrap_or(&TuiKeyBinding::new(KeyCode::Char('n'))), + address_mode_both: *value + .get(&TuiCommandItem::AddressModeBoth) + .unwrap_or(&TuiKeyBinding::new(KeyCode::Char('b'))), + toggle_freeze: *value + .get(&TuiCommandItem::ToggleFreeze) + .unwrap_or(&TuiKeyBinding::new(KeyCode::Char('f'))), + toggle_chart: *value + .get(&TuiCommandItem::ToggleChart) + .unwrap_or(&TuiKeyBinding::new(KeyCode::Char('c'))), + expand_hosts: *value + .get(&TuiCommandItem::ExpandHosts) + .unwrap_or(&TuiKeyBinding::new(KeyCode::Char(']'))), + contract_hosts: *value + .get(&TuiCommandItem::ContractHosts) + .unwrap_or(&TuiKeyBinding::new(KeyCode::Char('['))), + expand_hosts_max: *value + .get(&TuiCommandItem::ExpandHostsMax) + .unwrap_or(&TuiKeyBinding::new(KeyCode::Char('}'))), + contract_hosts_min: *value + .get(&TuiCommandItem::ContractHostsMin) + .unwrap_or(&TuiKeyBinding::new(KeyCode::Char('{'))), + chart_zoom_in: *value + .get(&TuiCommandItem::ChartZoomIn) + .unwrap_or(&TuiKeyBinding::new(KeyCode::Char('='))), + chart_zoom_out: *value + .get(&TuiCommandItem::ChartZoomOut) + .unwrap_or(&TuiKeyBinding::new(KeyCode::Char('-'))), + clear_trace_data: *value.get(&TuiCommandItem::ClearTraceData).unwrap_or( + &TuiKeyBinding::new_with_modifier(KeyCode::Char('r'), KeyModifiers::CONTROL), + ), + clear_dns_cache: *value.get(&TuiCommandItem::ClearDnsCache).unwrap_or( + &TuiKeyBinding::new_with_modifier(KeyCode::Char('k'), KeyModifiers::CONTROL), + ), + clear_selection: *value + .get(&TuiCommandItem::ClearSelection) + .unwrap_or(&TuiKeyBinding::new(KeyCode::Esc)), + toggle_as_info: *value + .get(&TuiCommandItem::ToggleASInfo) + .unwrap_or(&TuiKeyBinding::new(KeyCode::Char('z'))), + quit: *value + .get(&TuiCommandItem::Quit) + .unwrap_or(&TuiKeyBinding::new(KeyCode::Char('q'))), + } + } +} + +/// Tui key binding. +#[derive(Debug, Clone, Copy)] +pub struct TuiKeyBinding { + pub code: KeyCode, + pub modifier: KeyModifiers, +} + +impl TuiKeyBinding { + pub fn new(code: KeyCode) -> Self { + Self { + code, + modifier: KeyModifiers::NONE, + } + } + + pub fn new_with_modifier(code: KeyCode, modifier: KeyModifiers) -> Self { + Self { code, modifier } + } +} + +impl TryFrom<&str> for TuiKeyBinding { + type Error = anyhow::Error; + + fn try_from(value: &str) -> Result { + const ALL_MODIFIERS: [(&str, KeyModifiers); 6] = [ + ("shift", KeyModifiers::SHIFT), + ("ctrl", KeyModifiers::CONTROL), + ("alt", KeyModifiers::ALT), + ("super", KeyModifiers::SUPER), + ("hyper", KeyModifiers::HYPER), + ("meta", KeyModifiers::META), + ]; + const ALL_SPECIAL_KEYS: [(&str, KeyCode); 16] = [ + ("backspace", KeyCode::Backspace), + ("enter", KeyCode::Enter), + ("left", KeyCode::Left), + ("right", KeyCode::Right), + ("up", KeyCode::Up), + ("down", KeyCode::Down), + ("home", KeyCode::Home), + ("end", KeyCode::End), + ("pageup", KeyCode::PageUp), + ("pagedown", KeyCode::PageDown), + ("tab", KeyCode::Tab), + ("backtab", KeyCode::BackTab), + ("delete", KeyCode::Delete), + ("insert", KeyCode::Insert), + ("null", KeyCode::Null), + ("esc", KeyCode::Esc), + ]; + fn parse_keycode(value: &str) -> anyhow::Result { + Ok(if value.len() == 1 { + KeyCode::Char(char::from_str(value)?.to_ascii_lowercase()) + } else { + ALL_SPECIAL_KEYS + .iter() + .find_map(|(keycode_str, keycode)| { + if keycode_str.eq_ignore_ascii_case(value) { + Some(*keycode) + } else { + None + } + }) + .ok_or_else(|| anyhow!("unknown key binding '{}'", value))? + }) + } + fn parse_modifiers(modifiers: &str) -> anyhow::Result { + modifiers + .split('+') + .fold(Ok(KeyModifiers::NONE), |key_modifiers, token| { + key_modifiers.and_then(|modifiers| { + ALL_MODIFIERS + .iter() + .find_map(|(modifier_token, modifier)| { + if modifier_token.eq_ignore_ascii_case(token) { + Some(modifiers | *modifier) + } else { + None + } + }) + .ok_or_else(|| anyhow!("unknown modifier '{}'", token,)) + }) + }) + } + match value.rsplit_once('+') { + Some((modifiers, value)) => Ok(Self { + code: parse_keycode(value)?, + modifier: parse_modifiers(modifiers)?, + }), + None => Ok(Self { + code: parse_keycode(value)?, + modifier: KeyModifiers::NONE, + }), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use test_case::test_case; + + #[test_case("c", KeyCode::Char('c'), KeyModifiers::NONE; "char without any modifier")] + #[test_case("1", KeyCode::Char('1'), KeyModifiers::NONE; "number without any modifier")] + #[test_case(",", KeyCode::Char(','), KeyModifiers::NONE; "punctuation without any modifier")] + #[test_case("backspace", KeyCode::Backspace, KeyModifiers::NONE; "backspace without any modifier")] + #[test_case("enter", KeyCode::Enter, KeyModifiers::NONE; "enter without any modifier")] + #[test_case("left", KeyCode::Left, KeyModifiers::NONE; "left without any modifier")] + #[test_case("right", KeyCode::Right, KeyModifiers::NONE; "right without any modifier")] + #[test_case("up", KeyCode::Up, KeyModifiers::NONE; "up without any modifier")] + #[test_case("down", KeyCode::Down, KeyModifiers::NONE; "down without any modifier")] + #[test_case("home", KeyCode::Home, KeyModifiers::NONE; "home without any modifier")] + #[test_case("end", KeyCode::End, KeyModifiers::NONE; "end without any modifier")] + #[test_case("pageup", KeyCode::PageUp, KeyModifiers::NONE; "pageup without any modifier")] + #[test_case("pagedown", KeyCode::PageDown, KeyModifiers::NONE; "pagedown without any modifier")] + #[test_case("tab", KeyCode::Tab, KeyModifiers::NONE; "tab without any modifier")] + #[test_case("backtab", KeyCode::BackTab, KeyModifiers::NONE; "backtab without any modifier")] + #[test_case("delete", KeyCode::Delete, KeyModifiers::NONE; "delete without any modifier")] + #[test_case("insert", KeyCode::Insert, KeyModifiers::NONE; "insert without any modifier")] + #[test_case("null", KeyCode::Null, KeyModifiers::NONE; "null without any modifier")] + #[test_case("esc", KeyCode::Esc, KeyModifiers::NONE; "escape without any modifier")] + #[test_case("shift+c", KeyCode::Char('c'), KeyModifiers::SHIFT; "with shift modifier")] + #[test_case("ctrl+i", KeyCode::Char('i'), KeyModifiers::CONTROL; "i with ctrl modifier")] + #[test_case("shift+I", KeyCode::Char('i'), KeyModifiers::SHIFT; "I with shift modifier")] + #[test_case("alt+c", KeyCode::Char('c'), KeyModifiers::ALT; "with alt modifier")] + #[test_case("super+c", KeyCode::Char('c'), KeyModifiers::SUPER; "with super modifier")] + #[test_case("hyper+c", KeyCode::Char('c'), KeyModifiers::HYPER; "with hyper modifier")] + #[test_case("meta+c", KeyCode::Char('c'), KeyModifiers::META; "with meta modifier")] + #[test_case("alt+shift+k", KeyCode::Char('k'), KeyModifiers::ALT | KeyModifiers::SHIFT; "with alt shift modifier")] + #[test_case("ctrl+up", KeyCode::Up, KeyModifiers::CONTROL; "up with ctrl modifier")] + #[test_case("shift+ctrl+alt+super+hyper+meta+k", KeyCode::Char('k'), KeyModifiers::all(); "with all modifiers")] + fn test_key_binding(input: &str, code: KeyCode, modifiers: KeyModifiers) -> anyhow::Result<()> { + let binding = TuiKeyBinding::try_from(input)?; + assert_eq!(binding.code, code); + assert_eq!(binding.modifier, modifiers); + Ok(()) + } + + #[test] + fn test_unknown_modifier() { + let binding = TuiKeyBinding::try_from("foo+c"); + assert!(binding.is_err()); + assert_eq!(&binding.unwrap_err().to_string(), "unknown modifier 'foo'"); + } + + #[test] + fn test_unknown_second_modifier() { + let binding = TuiKeyBinding::try_from("alt+foo+c"); + assert!(binding.is_err()); + assert_eq!(&binding.unwrap_err().to_string(), "unknown modifier 'foo'"); + } + + #[test] + fn test_unknown_key() { + let binding = TuiKeyBinding::try_from("foo"); + assert!(binding.is_err()); + assert_eq!( + &binding.unwrap_err().to_string(), + "unknown key binding 'foo'" + ); + } +} + +/// A Tui command that can be bound to a key. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, EnumString, EnumVariantNames)] +#[strum(serialize_all = "kebab-case")] +#[allow(clippy::enum_variant_names)] +pub enum TuiCommandItem { + /// Toggle the help dialog. + ToggleHelp, + /// Move down to the next hop. + NextHop, + /// Move up to the previous hop. + PreviousHop, + /// Move right to the next trace. + NextTrace, + /// Move left to the previous trace. + PreviousTrace, + /// Show IP address mode. + AddressModeIp, + /// Show hostname mode. + AddressModeHost, + /// Show hostname and IP address mode. + AddressModeBoth, + /// Toggle freezing the display. + ToggleFreeze, + /// Toggle the chart. + ToggleChart, + /// Expand hosts. + ExpandHosts, + /// Expand hosts to max. + ExpandHostsMax, + /// Contract hosts. + ContractHosts, + /// Contract hosts to min. + ContractHostsMin, + /// Zoom chart in. + ChartZoomIn, + /// Zoom chart out. + ChartZoomOut, + /// Clear all tracing data. + ClearTraceData, + /// Clear DNS cache. + ClearDnsCache, + /// Clear hop selection. + ClearSelection, + /// Toggle AS info. + ToggleASInfo, + /// Quit the application. + Quit, +} + impl TryFrom<(Args, u16)> for TrippyConfig { type Error = anyhow::Error; @@ -530,6 +857,13 @@ impl TryFrom<(Args, u16)> for TrippyConfig { ); process::exit(0); } + if args.print_tui_binding_commands { + println!( + "TUI binding commands: {}", + TuiCommandItem::VARIANTS.join(", ") + ); + process::exit(0); + } let protocol = match (args.udp, args.tcp, args.protocol) { (false, false, Protocol::Icmp) => TracerProtocol::Icmp, (false, false, Protocol::Udp) | (true, _, _) => TracerProtocol::Udp, @@ -610,6 +944,11 @@ impl TryFrom<(Args, u16)> for TrippyConfig { .into_iter() .collect::>(), ); + let tui_bindings = TuiBindings::from( + args.tui_key_bindings + .into_iter() + .collect::>(), + ); Ok(Self { targets: args.targets, protocol, @@ -638,6 +977,7 @@ impl TryFrom<(Args, u16)> for TrippyConfig { tui_address_mode: args.tui_address_mode, tui_max_addrs: args.tui_max_addrs, tui_theme, + tui_bindings, mode: args.mode, report_cycles: args.report_cycles, max_rounds, diff --git a/src/frontend.rs b/src/frontend.rs index 98a75b5be..3d2718e7a 100644 --- a/src/frontend.rs +++ b/src/frontend.rs @@ -1,9 +1,11 @@ use crate::backend::Hop; -use crate::config::{AddressMode, DnsResolveMethod, TuiColor, TuiTheme}; +use crate::config::{ + AddressMode, DnsResolveMethod, TuiBindings, TuiColor, TuiKeyBinding, TuiTheme, +}; use crate::dns::{DnsEntry, Resolved}; use crate::{DnsResolver, Trace, TraceInfo}; use chrono::SecondsFormat; -use crossterm::event::KeyModifiers; +use crossterm::event::{KeyEvent, KeyModifiers}; use crossterm::{ event::{self, Event, KeyCode}, execute, @@ -82,6 +84,93 @@ const HELP_LINES: [&str; 16] = [ "q - quit", ]; +/// Tui key bindings. +#[derive(Debug, Clone, Copy)] +pub struct Bindings { + toggle_help: KeyBinding, + up: KeyBinding, + down: KeyBinding, + left: KeyBinding, + right: KeyBinding, + address_mode_ip: KeyBinding, + address_mode_host: KeyBinding, + address_mode_both: KeyBinding, + toggle_freeze: KeyBinding, + toggle_chart: KeyBinding, + expand_hosts: KeyBinding, + contract_hosts: KeyBinding, + expand_hosts_max: KeyBinding, + contract_hosts_min: KeyBinding, + chart_zoom_in: KeyBinding, + chart_zoom_out: KeyBinding, + clear_trace_data: KeyBinding, + clear_dns_cache: KeyBinding, + clear_selection: KeyBinding, + toggle_as_info: KeyBinding, + quit: KeyBinding, +} + +impl From for Bindings { + fn from(value: TuiBindings) -> Self { + Self { + toggle_help: KeyBinding::from(value.toggle_help), + up: KeyBinding::from(value.up), + down: KeyBinding::from(value.down), + left: KeyBinding::from(value.left), + right: KeyBinding::from(value.right), + address_mode_ip: KeyBinding::from(value.address_mode_ip), + address_mode_host: KeyBinding::from(value.address_mode_host), + address_mode_both: KeyBinding::from(value.address_mode_both), + toggle_freeze: KeyBinding::from(value.toggle_freeze), + toggle_chart: KeyBinding::from(value.toggle_chart), + expand_hosts: KeyBinding::from(value.expand_hosts), + contract_hosts: KeyBinding::from(value.contract_hosts), + expand_hosts_max: KeyBinding::from(value.expand_hosts_max), + contract_hosts_min: KeyBinding::from(value.contract_hosts_min), + chart_zoom_in: KeyBinding::from(value.chart_zoom_in), + chart_zoom_out: KeyBinding::from(value.chart_zoom_out), + clear_trace_data: KeyBinding::from(value.clear_trace_data), + clear_dns_cache: KeyBinding::from(value.clear_dns_cache), + clear_selection: KeyBinding::from(value.clear_selection), + toggle_as_info: KeyBinding::from(value.toggle_as_info), + quit: KeyBinding::from(value.quit), + } + } +} + +const CTRL_C: KeyBinding = KeyBinding { + code: KeyCode::Char('c'), + modifiers: KeyModifiers::CONTROL, +}; + +/// Tui key binding. +#[derive(Debug, Clone, Copy)] +pub struct KeyBinding { + pub code: KeyCode, + pub modifiers: KeyModifiers, +} + +impl KeyBinding { + pub fn check(&self, event: KeyEvent) -> bool { + let code_match = match (event.code, self.code) { + (KeyCode::Char(c1), KeyCode::Char(c2)) => { + c1.to_ascii_lowercase() == c2.to_ascii_lowercase() + } + (c1, c2) => c1 == c2, + }; + code_match && self.modifiers == event.modifiers + } +} + +impl From for KeyBinding { + fn from(value: TuiKeyBinding) -> Self { + Self { + code: value.code, + modifiers: value.modifier, + } + } +} + /// Tui color theme. #[derive(Debug, Clone, Copy)] pub struct Theme { @@ -191,9 +280,12 @@ pub struct TuiConfig { max_samples: usize, /// The Tui color theme. theme: Theme, + /// The Tui keyboard bindings. + bindings: Bindings, } impl TuiConfig { + #[allow(clippy::too_many_arguments)] pub fn new( refresh_rate: Duration, preserve_screen: bool, @@ -202,6 +294,7 @@ impl TuiConfig { max_addrs: Option, max_samples: usize, tui_theme: TuiTheme, + tui_bindings: TuiBindings, ) -> Self { Self { refresh_rate, @@ -211,6 +304,7 @@ impl TuiConfig { max_addrs, max_samples, theme: Theme::from(tui_theme), + bindings: Bindings::from(tui_bindings), } } } @@ -339,7 +433,13 @@ impl TuiApp { } fn toggle_asinfo(&mut self) { - self.tui_config.lookup_as_info = !self.tui_config.lookup_as_info; + match self.resolver.config().resolve_method { + DnsResolveMethod::Resolv | DnsResolveMethod::Google | DnsResolveMethod::Cloudflare => { + self.tui_config.lookup_as_info = !self.tui_config.lookup_as_info; + self.resolver.flush(); + } + DnsResolveMethod::System => {} + } } fn expand_hosts(&mut self) { @@ -428,63 +528,59 @@ fn run_app( terminal.draw(|f| render_app(f, &mut app))?; if event::poll(app.tui_config.refresh_rate)? { if let Event::Key(key) = event::read()? { + let bindings = &app.tui_config.bindings; if app.show_help { - match key.code { - KeyCode::Char('q' | 'h') | KeyCode::Esc => app.toggle_help(), - _ => {} - } - } else { - match (key.code, key.modifiers) { - (KeyCode::Char('h'), _) => app.toggle_help(), - (KeyCode::Char('q'), _) | (KeyCode::Char('c'), KeyModifiers::CONTROL) => { - return Ok(()) - } - (KeyCode::Char('f'), _) => app.toggle_freeze(), - (KeyCode::Char('c'), _) => app.toggle_chart(), - (KeyCode::Char('r'), KeyModifiers::CONTROL) => { - app.clear(); - app.clear_trace_data(); - } - (KeyCode::Char('k'), KeyModifiers::CONTROL) => { - app.resolver.flush(); - } - (KeyCode::Down, _) => app.next_hop(), - (KeyCode::Up, _) => app.previous_hop(), - (KeyCode::Esc, _) => app.clear(), - (KeyCode::Left, _) => { - app.previous_trace(); - app.clear(); - } - (KeyCode::Right, _) => { - app.next_trace(); - app.clear(); - } - (KeyCode::Char('i'), _) => { - app.tui_config.address_mode = AddressMode::IP; - } - (KeyCode::Char('n'), _) => { - app.tui_config.address_mode = AddressMode::Host; - } - (KeyCode::Char('b'), _) => { - app.tui_config.address_mode = AddressMode::Both; - } - (KeyCode::Char('z'), _) => match app.resolver.config().resolve_method { - DnsResolveMethod::Resolv - | DnsResolveMethod::Google - | DnsResolveMethod::Cloudflare => { - app.toggle_asinfo(); - app.resolver.flush(); - } - DnsResolveMethod::System => {} - }, - (KeyCode::Char('{'), _) => app.contract_hosts_min(), - (KeyCode::Char('}'), _) => app.expand_hosts_max(), - (KeyCode::Char('['), _) => app.contract_hosts(), - (KeyCode::Char(']'), _) => app.expand_hosts(), - (KeyCode::Char('+' | '='), _) => app.zoom_in(), - (KeyCode::Char('-'), _) => app.zoom_out(), - _ => {} + if bindings.toggle_help.check(key) + || bindings.clear_selection.check(key) + || bindings.quit.check(key) + { + app.toggle_help(); } + } else if bindings.toggle_help.check(key) { + app.toggle_help(); + } else if bindings.down.check(key) { + app.next_hop(); + } else if bindings.up.check(key) { + app.previous_hop(); + } else if bindings.left.check(key) { + app.previous_trace(); + app.clear(); + } else if bindings.right.check(key) { + app.next_trace(); + app.clear(); + } else if bindings.address_mode_ip.check(key) { + app.tui_config.address_mode = AddressMode::IP; + } else if bindings.address_mode_host.check(key) { + app.tui_config.address_mode = AddressMode::Host; + } else if bindings.address_mode_both.check(key) { + app.tui_config.address_mode = AddressMode::Both; + } else if bindings.toggle_freeze.check(key) { + app.toggle_freeze(); + } else if bindings.toggle_chart.check(key) { + app.toggle_chart(); + } else if bindings.contract_hosts_min.check(key) { + app.contract_hosts_min(); + } else if bindings.expand_hosts_max.check(key) { + app.expand_hosts_max(); + } else if bindings.contract_hosts.check(key) { + app.contract_hosts(); + } else if bindings.expand_hosts.check(key) { + app.expand_hosts(); + } else if bindings.chart_zoom_in.check(key) { + app.zoom_in(); + } else if bindings.chart_zoom_out.check(key) { + app.zoom_out(); + } else if bindings.clear_trace_data.check(key) { + app.clear(); + app.clear_trace_data(); + } else if bindings.clear_dns_cache.check(key) { + app.resolver.flush(); + } else if bindings.clear_selection.check(key) { + app.clear(); + } else if bindings.toggle_as_info.check(key) { + app.toggle_asinfo(); + } else if bindings.quit.check(key) || CTRL_C.check(key) { + return Ok(()); } } } diff --git a/src/main.rs b/src/main.rs index 11d843300..0bb871210 100644 --- a/src/main.rs +++ b/src/main.rs @@ -215,6 +215,7 @@ fn make_tui_config(args: &TrippyConfig) -> TuiConfig { args.tui_max_addrs, args.tui_max_samples, args.tui_theme, + args.tui_bindings, ) }