use std::{fmt, ops::Deref, sync::OnceLock};
use log::warn;
use crate::config::KdashConfig;
use crate::event::Key;
macro_rules! generate_keybindings {
($($field:ident),+) => {
#[derive(Clone)]
pub struct KeyBindings { $(pub $field: KeyBinding),+ }
impl KeyBindings {
pub fn as_iter(&self) -> Vec<&KeyBinding> {
vec![
$(&self.$field),+
]
}
pub fn with_overrides(&self, config: &KdashConfig) -> (Self, Vec<String>) {
let mut updated = self.clone();
let mut warnings = vec![];
if let Some(overrides) = &config.keybindings {
$(
if let Some(value) = overrides.values.get(stringify!($field)) {
match value.parse::<Key>() {
Ok(key) => updated.$field.key = key,
Err(error) => warnings.push(format!(
"Invalid keybinding override for {}: {} ({})",
stringify!($field),
value,
error
)),
}
}
)+
for field in overrides.values.keys() {
if ![ $(stringify!($field)),+ ].contains(&field.as_str()) {
warnings.push(format!("Unknown keybinding override: {}", field));
}
}
}
(updated, warnings)
}
}
};
}
generate_keybindings! {
quit,
esc,
help,
submit,
filter,
refresh,
toggle_theme,
cycle_theme_prev,
cycle_main_views,
cycle_main_views_prev,
reset_navigation,
jump_to_current_context,
jump_to_all_context,
jump_to_utilization,
jump_to_troubleshoot,
copy_to_clipboard,
dump_error_log,
pg_up,
pg_down,
home,
end,
up,
down,
left,
right,
toggle_info,
shell_exec,
log_auto_scroll,
select_all_namespace,
open_action_menu,
delete_resource,
restart_resource,
previous_logs,
jump_to_namespace,
describe_resource,
resource_yaml,
edit_resource,
decode_secret,
jump_to_pods,
jump_to_services,
jump_to_nodes,
jump_to_configmaps,
jump_to_statefulsets,
jump_to_replicasets,
jump_to_deployments,
jump_to_jobs,
jump_to_daemonsets,
jump_to_more_resources,
jump_to_dynamic_resources,
aggregate_logs,
port_forward,
port_forwards_list,
cycle_group_by,
toggle_wide_columns,
toggle_log_timestamps,
toggle_log_wrap
}
#[derive(PartialEq, Eq, Clone, Copy, Hash, Debug)]
pub enum HContext {
General,
Overview,
Utilization,
Logs,
}
impl fmt::Display for HContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
impl HContext {
fn label(self) -> &'static str {
match self {
HContext::General => "General",
HContext::Overview => "Resource Views",
HContext::Utilization => "Utilization",
HContext::Logs => "Logs",
}
}
}
#[derive(Clone)]
pub struct KeyBinding {
pub key: Key,
pub alt: Option<Key>,
pub desc: &'static str,
pub context: HContext,
}
const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings {
quit: KeyBinding {
key: Key::Ctrl('c'),
alt: Some(Key::Char('q')),
desc: "Quit",
context: HContext::General,
},
esc: KeyBinding {
key: Key::Esc,
alt: None,
desc: "Close child page/Go back",
context: HContext::General,
},
help: KeyBinding {
key: Key::Char('?'),
alt: None,
desc: "Help page",
context: HContext::General,
},
submit: KeyBinding {
key: Key::Enter,
alt: None,
desc: "Select table row",
context: HContext::General,
},
filter: KeyBinding {
key: Key::Char('/'),
alt: None,
desc: "Filter current view",
context: HContext::General,
},
refresh: KeyBinding {
key: Key::Ctrl('r'),
alt: None,
desc: "Refresh data",
context: HContext::General,
},
toggle_theme: KeyBinding {
key: Key::Char('t'),
alt: None,
desc: "Cycle theme (next)",
context: HContext::General,
},
cycle_theme_prev: KeyBinding {
key: Key::Alt('t'),
alt: None,
desc: "Cycle theme (previous)",
context: HContext::General,
},
jump_to_current_context: KeyBinding {
key: Key::Shift('a'),
alt: None,
desc: "Switch to active context view",
context: HContext::General,
},
jump_to_all_context: KeyBinding {
key: Key::Shift('c'),
alt: None,
desc: "Switch to all contexts view",
context: HContext::General,
},
jump_to_utilization: KeyBinding {
key: Key::Shift('u'),
alt: None,
desc: "Switch to resource utilization view",
context: HContext::General,
},
jump_to_troubleshoot: KeyBinding {
key: Key::Shift('t'),
alt: None,
desc: "Switch to troubleshoot view",
context: HContext::General,
},
cycle_main_views: KeyBinding {
key: Key::Tab,
alt: None,
desc: "Cycle through main views",
context: HContext::General,
},
cycle_main_views_prev: KeyBinding {
key: Key::BackTab,
alt: None,
desc: "Cycle through main views (previous)",
context: HContext::General,
},
reset_navigation: KeyBinding {
key: Key::Ctrl('h'),
alt: None,
desc: "Reset navigation to root view",
context: HContext::General,
},
copy_to_clipboard: KeyBinding {
key: Key::Char('c'),
alt: None,
desc: "Copy log/output to clipboard",
context: HContext::General,
},
dump_error_log: KeyBinding {
key: Key::Shift('d'),
alt: None,
desc: "Dump recent errors to file",
context: HContext::General,
},
down: KeyBinding {
key: Key::Down,
alt: Some(Key::Char('j')),
desc: "Next row/Scroll down",
context: HContext::General,
},
up: KeyBinding {
key: Key::Up,
alt: Some(Key::Char('k')),
desc: "Previous row/Scroll up",
context: HContext::General,
},
pg_up: KeyBinding {
key: Key::PageUp,
alt: None,
desc: "Scroll page up",
context: HContext::General,
},
pg_down: KeyBinding {
key: Key::PageDown,
alt: None,
desc: "Scroll page down",
context: HContext::General,
},
home: KeyBinding {
key: Key::Home,
alt: None,
desc: "Scroll to top",
context: HContext::General,
},
end: KeyBinding {
key: Key::End,
alt: None,
desc: "Scroll to bottom",
context: HContext::General,
},
left: KeyBinding {
key: Key::Left,
alt: Some(Key::Char('h')),
desc: "Next resource tab",
context: HContext::Overview,
},
right: KeyBinding {
key: Key::Right,
alt: Some(Key::Char('l')),
desc: "Previous resource tab",
context: HContext::Overview,
},
toggle_info: KeyBinding {
key: Key::Char('i'),
alt: None,
desc: "Show/Hide info bar",
context: HContext::Overview,
},
shell_exec: KeyBinding {
key: Key::Char('s'),
alt: None,
desc: "Open shell in selected container",
context: HContext::Overview,
},
log_auto_scroll: KeyBinding {
key: Key::Char('s'),
alt: None,
desc: "Toggle log auto scroll",
context: HContext::Overview,
},
jump_to_namespace: KeyBinding {
key: Key::Char('n'),
alt: None,
desc: "Select namespace block",
context: HContext::Overview,
},
select_all_namespace: KeyBinding {
key: Key::Char('a'),
alt: None,
desc: "Select all namespaces",
context: HContext::Overview,
},
open_action_menu: KeyBinding {
key: Key::Char('m'),
alt: None,
desc: "Show actions menu for selected resource",
context: HContext::Overview,
},
delete_resource: KeyBinding {
key: Key::Ctrl('d'),
alt: None,
desc: "Delete selected resource",
context: HContext::Overview,
},
restart_resource: KeyBinding {
key: Key::Char('r'),
alt: None,
desc: "Rollout restart workload (deploy/sts/ds)",
context: HContext::Overview,
},
previous_logs: KeyBinding {
key: Key::Char('p'),
alt: None,
desc: "Show previous (restarted) container logs",
context: HContext::Overview,
},
describe_resource: KeyBinding {
key: Key::Char('d'),
alt: None,
desc: "Describe resource",
context: HContext::Overview,
},
resource_yaml: KeyBinding {
key: Key::Char('y'),
alt: None,
desc: "Get resource YAML",
context: HContext::Overview,
},
edit_resource: KeyBinding {
key: Key::Char('e'),
alt: None,
desc: "Edit resource in $EDITOR",
context: HContext::Overview,
},
decode_secret: KeyBinding {
key: Key::Char('x'),
alt: None,
desc: "Decode secret",
context: HContext::Overview,
},
jump_to_pods: KeyBinding {
key: Key::Char('1'),
alt: None,
desc: "Select pods tab",
context: HContext::Overview,
},
jump_to_services: KeyBinding {
key: Key::Char('2'),
alt: None,
desc: "Select services tab",
context: HContext::Overview,
},
jump_to_nodes: KeyBinding {
key: Key::Char('3'),
alt: None,
desc: "Select nodes tab",
context: HContext::Overview,
},
jump_to_configmaps: KeyBinding {
key: Key::Char('4'),
alt: None,
desc: "Select configmaps tab",
context: HContext::Overview,
},
jump_to_statefulsets: KeyBinding {
key: Key::Char('5'),
alt: None,
desc: "Select replicasets tab",
context: HContext::Overview,
},
jump_to_replicasets: KeyBinding {
key: Key::Char('6'),
alt: None,
desc: "Select statefulsets tab",
context: HContext::Overview,
},
jump_to_deployments: KeyBinding {
key: Key::Char('7'),
alt: None,
desc: "Select deployments tab",
context: HContext::Overview,
},
jump_to_jobs: KeyBinding {
key: Key::Char('8'),
alt: None,
desc: "Select jobs tab",
context: HContext::Overview,
},
jump_to_daemonsets: KeyBinding {
key: Key::Char('9'),
alt: None,
desc: "Select daemon sets tab",
context: HContext::Overview,
},
jump_to_more_resources: KeyBinding {
key: Key::Char('0'),
alt: None,
desc: "Select more resources",
context: HContext::Overview,
},
jump_to_dynamic_resources: KeyBinding {
key: Key::Char('-'),
alt: None,
desc: "Select dynamic resources",
context: HContext::Overview,
},
aggregate_logs: KeyBinding {
key: Key::Shift('l'),
alt: None,
desc: "Aggregate logs for resource",
context: HContext::Overview,
},
port_forward: KeyBinding {
key: Key::Char('f'),
alt: None,
desc: "Port-forward selected pod/service",
context: HContext::Overview,
},
port_forwards_list: KeyBinding {
key: Key::Shift('f'),
alt: None,
desc: "List/stop active port-forwards",
context: HContext::Overview,
},
cycle_group_by: KeyBinding {
key: Key::Char('g'),
alt: None,
desc: "Cycle through grouping",
context: HContext::Utilization,
},
toggle_wide_columns: KeyBinding {
key: Key::Char('w'),
alt: None,
desc: "Toggle wide view (show all columns)",
context: HContext::General,
},
toggle_log_timestamps: KeyBinding {
key: Key::Char('t'),
alt: None,
desc: "Toggle log timestamps",
context: HContext::Logs,
},
toggle_log_wrap: KeyBinding {
key: Key::Char('w'),
alt: None,
desc: "Toggle log line wrap",
context: HContext::Logs,
},
};
static ACTIVE_KEYBINDINGS: OnceLock<KeyBindings> = OnceLock::new();
pub struct ActiveKeyBindings;
impl Deref for ActiveKeyBindings {
type Target = KeyBindings;
fn deref(&self) -> &Self::Target {
ACTIVE_KEYBINDINGS.get_or_init(|| DEFAULT_KEYBINDINGS.clone())
}
}
pub static DEFAULT_KEYBINDING: ActiveKeyBindings = ActiveKeyBindings;
pub fn initialize_keybindings(config: &KdashConfig) -> Vec<String> {
let (keybindings, warnings) = DEFAULT_KEYBINDINGS.with_overrides(config);
let _ = ACTIVE_KEYBINDINGS.set(keybindings);
for warning in &warnings {
warn!("{}", warning);
}
warnings
}
pub struct HelpSection {
pub title: &'static str,
pub context: HContext,
pub rows: Vec<(String, String)>,
}
pub fn get_help_sections() -> Vec<HelpSection> {
let items = DEFAULT_KEYBINDING.as_iter();
[
HContext::General,
HContext::Overview,
HContext::Utilization,
HContext::Logs,
]
.into_iter()
.filter_map(|ctx| {
let rows: Vec<(String, String)> = items
.iter()
.filter(|b| b.context == ctx)
.map(|b| (key_label(b), b.desc.to_string()))
.collect();
(!rows.is_empty()).then_some(HelpSection {
title: ctx.label(),
context: ctx,
rows,
})
})
.collect()
}
fn key_label(item: &KeyBinding) -> String {
match item.alt {
Some(alt) => format!("{},{}", item.key.symbol(), alt.symbol()),
None => item.key.symbol(),
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use super::{DEFAULT_KEYBINDINGS, *};
use crate::config::{KdashConfig, KeybindingOverrides};
#[test]
fn test_as_iter() {
assert!(DEFAULT_KEYBINDING.as_iter().len() >= 28);
}
#[test]
fn test_with_overrides_updates_known_binding() {
let config = KdashConfig {
keybindings: Some(KeybindingOverrides {
values: BTreeMap::from([("quit".into(), "ctrl+q".into())]),
}),
..Default::default()
};
let (keybindings, warnings) = DEFAULT_KEYBINDINGS.with_overrides(&config);
assert_eq!(keybindings.quit.key, Key::Ctrl('q'));
assert!(warnings.is_empty());
}
#[test]
fn test_with_overrides_treats_uppercase_and_shift_equally() {
let uppercase = KdashConfig {
keybindings: Some(KeybindingOverrides {
values: BTreeMap::from([("dump_error_log".into(), "D".into())]),
}),
..Default::default()
};
let shifted = KdashConfig {
keybindings: Some(KeybindingOverrides {
values: BTreeMap::from([("dump_error_log".into(), "shift+d".into())]),
}),
..Default::default()
};
let (uppercase_bindings, uppercase_warnings) = DEFAULT_KEYBINDINGS.with_overrides(&uppercase);
let (shifted_bindings, shifted_warnings) = DEFAULT_KEYBINDINGS.with_overrides(&shifted);
assert_eq!(uppercase_bindings.dump_error_log.key, Key::Shift('d'));
assert_eq!(
uppercase_bindings.dump_error_log.key,
shifted_bindings.dump_error_log.key
);
assert!(uppercase_warnings.is_empty());
assert!(shifted_warnings.is_empty());
}
#[test]
fn test_with_overrides_warns_on_invalid_or_unknown_binding() {
let config = KdashConfig {
keybindings: Some(KeybindingOverrides {
values: BTreeMap::from([
("quit".into(), "boguskey".into()),
("made_up".into(), "x".into()),
]),
}),
..Default::default()
};
let (_, warnings) = DEFAULT_KEYBINDINGS.with_overrides(&config);
assert_eq!(warnings.len(), 2);
assert!(warnings.iter().any(|warning| warning.contains("quit")));
assert!(warnings.iter().any(|warning| warning.contains("made_up")));
}
}