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_main_views,
jump_to_current_context,
jump_to_all_context,
jump_to_utilization,
jump_to_troubleshoot,
copy_to_clipboard,
dump_error_log,
pg_up,
pg_down,
up,
down,
left,
right,
toggle_info,
shell_exec,
log_auto_scroll,
select_all_namespace,
jump_to_namespace,
describe_resource,
resource_yaml,
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,
cycle_group_by,
toggle_wide_columns
}
#[derive(PartialEq, Eq, Clone, Copy, Hash, Debug)]
pub enum HContext {
General,
Overview,
Utilization,
}
impl fmt::Display for HContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
#[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: "Toggle theme",
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,
},
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,
},
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,
},
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,
},
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,
},
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,
},
};
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 fn get_help_docs() -> Vec<Vec<String>> {
let items = DEFAULT_KEYBINDING.as_iter();
items.iter().map(|it| help_row(it)).collect()
}
fn help_row(item: &KeyBinding) -> Vec<String> {
vec![
if let Some(alt) = item.alt {
format!("{} | {}", item.key, alt)
} else {
item.key.to_string()
},
String::from(item.desc),
item.context.to_string(),
]
}
#[cfg(test)]
mod tests {
use super::{DEFAULT_KEYBINDINGS, *};
use crate::config::{KdashConfig, KeybindingOverrides};
use std::collections::BTreeMap;
#[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(), "shift+tab".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")));
}
}