use super::*;
impl App {
pub fn handle_key(&mut self, key: KeyEvent) -> Result<()> {
if key.modifiers.contains(KeyModifiers::CONTROL) {
match key.code {
KeyCode::Char('c') => {
self.should_quit = true;
return Ok(());
}
KeyCode::Char('d') if self.mode == Mode::Table => {
self.request_delete(false);
return Ok(());
}
KeyCode::Char('k') if self.mode == Mode::Table => {
self.request_delete(true); return Ok(());
}
KeyCode::Char('r') if self.mode == Mode::Table => {
self.start_watch();
return Ok(());
}
_ => {}
}
}
match self.mode {
Mode::Table => self.key_table(key),
Mode::Command => self.key_command(key),
Mode::Filter => self.key_filter(key),
Mode::Detail | Mode::Diff | Mode::Events => self.key_scroll(key, true),
Mode::Logs => self.key_logs(key),
Mode::LogFilter => self.key_log_filter(key),
Mode::Help => {
if matches!(
key.code,
KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('?')
) {
self.mode = Mode::Table;
}
}
Mode::Namespaces => self.key_namespaces(key),
Mode::Contexts => self.key_contexts(key),
Mode::Containers => self.key_containers(key),
Mode::SetImage => self.key_set_image(key),
Mode::Confirm => self.key_confirm(key),
Mode::Prompt => self.key_prompt(key),
Mode::Pulse => self.key_pulse(key),
Mode::Xray => self.key_xray(key),
Mode::FluxMenu => self.key_flux_menu(key),
Mode::PortForwards => self.key_port_forwards(key),
Mode::Skins => self.key_skins(key),
}
Ok(())
}
pub(super) fn key_table(&mut self, key: KeyEvent) {
match key.code {
KeyCode::Char(':') => {
self.mode = Mode::Command;
self.command.clear();
self.update_suggestions();
}
KeyCode::Char('/') => self.mode = Mode::Filter,
KeyCode::Char('q') => self.should_quit = true,
KeyCode::Esc => {
if !self.marked.is_empty() {
self.marked.clear();
} else if !self.filter.is_empty() {
self.filter.clear();
self.invalidate_rows();
} else if !self.pop_frame() {
}
}
KeyCode::Char('j') | KeyCode::Down => self.move_selection(1),
KeyCode::Char('k') | KeyCode::Up => self.move_selection(-1),
KeyCode::Char('g') | KeyCode::Home => self.table_state.select(Some(0)),
KeyCode::Char('G') | KeyCode::End => {
let len = self.rows().len();
if len > 0 {
self.table_state.select(Some(len - 1));
}
}
KeyCode::PageDown => self.move_selection(10),
KeyCode::PageUp => self.move_selection(-10),
KeyCode::Char(' ') => {
self.toggle_mark();
self.move_selection(1);
}
KeyCode::Enter => self.drill(),
KeyCode::Char('y') => self.open_detail(),
KeyCode::Char('d') => self.describe(),
KeyCode::Char('E') => self.open_events(),
KeyCode::Char('l') => self.open_logs(),
KeyCode::Char('p') => self.open_previous_logs(),
KeyCode::Char('e') => self.request_edit(),
KeyCode::Char('s') => {
if self.kind_plural == "pods" {
self.request_exec();
} else {
self.request_scale();
}
}
KeyCode::Char('a') => self.request_attach(),
KeyCode::Char('i') => self.request_set_image(),
KeyCode::Char('o') => self.show_node(),
KeyCode::Char('c') => self.copy_name(),
KeyCode::Char('J') => self.jump_owner(),
KeyCode::Char('C') => self.request_cordon(true),
KeyCode::Char('U') => self.request_cordon(false),
KeyCode::Char('D') => self.request_drain(),
KeyCode::Char('S') => self.cycle_sort(),
KeyCode::Char('I') => self.toggle_sort_dir(),
KeyCode::Char('f') | KeyCode::Char('F') => self.request_port_forward(),
KeyCode::Char('n') => self.open_namespaces(),
KeyCode::Char('0') => {
self.namespace.clear();
self.flash = "namespace: all namespaces".into();
self.flash_err = false;
self.table_state.select(Some(0));
self.start_watch();
}
KeyCode::Char('r') => {
if matches!(
self.kind_plural.as_str(),
"deployments" | "statefulsets" | "daemonsets"
) {
self.request_restart();
} else if EXTERNAL_SECRET_KINDS.contains(&self.kind_plural.as_str()) {
self.request_refresh_es();
} else {
self.start_watch();
}
}
KeyCode::Char('t') => self.request_flux_menu(),
KeyCode::Char('?') => self.mode = Mode::Help,
KeyCode::Char(c) => self.try_plugin(c),
_ => {}
}
}
pub(super) fn try_plugin(&mut self, c: char) {
let Some(plugin) = self
.plugins
.iter()
.find(|p| {
p.key == c
&& (p.scopes.is_empty() || p.scopes.iter().any(|s| s == &self.kind_plural))
})
.cloned()
else {
return;
};
let Some(obj) = self.selected_ref() else {
self.flash_warn("no selection for plugin");
return;
};
let name = obj.metadata.name.clone().unwrap_or_default();
let ns = obj.metadata.namespace.clone().unwrap_or_default();
let ctx = self.cluster.context.clone();
let res = self.kind_plural.clone();
let subst = |s: &str| {
s.replace("$NAMESPACE", &ns)
.replace("$NS", &ns)
.replace("$NAME", &name)
.replace("$CONTEXT", &ctx)
.replace("$RESOURCE", &res)
};
let mut argv = vec![subst(&plugin.command)];
argv.extend(plugin.args.iter().map(|a| subst(a)));
self.flash = format!("plugin: {}", plugin.name);
self.flash_err = false;
self.pending = Some(Suspend::Shell(argv));
}
pub(super) fn key_command(&mut self, key: KeyEvent) {
match key.code {
KeyCode::Esc => self.mode = Mode::Table,
KeyCode::Down | KeyCode::Tab => {
if !self.cmd_suggestions.is_empty() {
self.cmd_sel = (self.cmd_sel + 1) % self.cmd_suggestions.len();
}
}
KeyCode::Up | KeyCode::BackTab => {
if !self.cmd_suggestions.is_empty() {
self.cmd_sel = self
.cmd_sel
.checked_sub(1)
.unwrap_or(self.cmd_suggestions.len() - 1);
}
}
KeyCode::Enter => {
let typed = self.command.trim().to_string();
let picked = self.cmd_suggestions.get(self.cmd_sel).cloned();
self.mode = Mode::Table;
self.command.clear();
if self.run_palette_command(&typed) {
} else if let Some(s) = picked {
match s.kind {
SuggestKind::Command => {
self.run_palette_command(&s.label);
}
SuggestKind::Resource => self.switch_kind(&s.label),
}
} else if !typed.is_empty() {
self.switch_kind(&typed);
}
}
KeyCode::Backspace => {
self.command.pop();
self.update_suggestions();
}
KeyCode::Char(c) => {
self.command.push(c);
self.update_suggestions();
}
_ => {}
}
}
pub(super) fn run_action(&mut self, action: PaletteAction) {
match action {
PaletteAction::Quit => self.should_quit = true,
PaletteAction::Ctx => self.open_contexts(),
PaletteAction::Pulse => self.open_pulse(),
PaletteAction::Xray => self.open_xray(),
PaletteAction::Diff => self.open_diff(),
PaletteAction::Events => self.open_events(),
PaletteAction::PortForwards => self.open_port_forwards(),
PaletteAction::Skin => self.open_skins(),
}
}
pub(super) fn run_palette_command(&mut self, cmd: &str) -> bool {
let cmd = cmd.trim();
if cmd.is_empty() {
return false;
}
let mut parts = cmd.split_whitespace();
if let Some(first) = parts.next()
&& first.eq_ignore_ascii_case("skin")
{
let rest = parts.collect::<Vec<_>>().join(" ");
if rest.is_empty() {
self.open_skins();
} else {
self.apply_skin(&rest);
}
return true;
}
let action = PALETTE_COMMANDS
.iter()
.find(|c| c.names.contains(&cmd))
.map(|c| c.action);
match action {
Some(a) => {
self.run_action(a);
true
}
None => false,
}
}
pub(super) fn update_suggestions(&mut self) {
let q = self.command.trim();
let mut scored: Vec<(i64, Suggestion)> = Vec::new();
if !q.is_empty() {
for c in PALETTE_COMMANDS {
let best = c
.names
.iter()
.filter_map(|n| self.matcher.fuzzy_match(n, q))
.max();
if let Some(score) = best {
scored.push((
score,
Suggestion {
label: c.names[0].to_string(),
kind: SuggestKind::Command,
},
));
}
}
}
let alias_target = if q.is_empty() {
None
} else {
self.cluster.resolve(q).map(|k| k.ar.plural.to_lowercase())
};
for c in self.cluster.catalog.iter().filter(|c| self.rbac_visible(c)) {
let score = if q.is_empty() {
Some(0)
} else if alias_target.as_deref() == Some(c.as_str()) {
Some(i64::MAX)
} else {
self.matcher.fuzzy_match(c, q)
};
if let Some(score) = score {
scored.push((
score,
Suggestion {
label: c.clone(),
kind: SuggestKind::Resource,
},
));
}
}
scored.sort_by(|a, b| b.0.cmp(&a.0).then_with(|| a.1.label.cmp(&b.1.label)));
self.cmd_suggestions = scored.into_iter().take(100).map(|(_, s)| s).collect();
self.cmd_sel = 0;
}
pub(super) fn key_filter(&mut self, key: KeyEvent) {
match key.code {
KeyCode::Esc => {
self.filter.clear();
self.mode = Mode::Table;
}
KeyCode::Enter => self.mode = Mode::Table,
KeyCode::Backspace => {
self.filter.pop();
}
KeyCode::Char(c) => self.filter.push(c),
_ => {}
}
self.invalidate_rows();
self.table_state.select(Some(0));
}
pub(super) fn key_scroll(&mut self, key: KeyEvent, detail: bool) {
let target = if detail {
&mut self.detail
} else {
&mut self.logs.view
};
match key.code {
KeyCode::Esc | KeyCode::Char('q') => {
if !detail {
self.stop_log_stream();
} else if self.mode == Mode::Events {
self.stop_event_stream();
}
self.mode = self.return_mode;
if self.return_mode == Mode::Table {
self.restore_selection();
}
}
KeyCode::Char('j') | KeyCode::Down => target.scroll_by(1),
KeyCode::Char('k') | KeyCode::Up => target.scroll_by(-1),
KeyCode::PageDown | KeyCode::Char(' ') => target.scroll_by(20),
KeyCode::PageUp => target.scroll_by(-20),
KeyCode::Char('g') | KeyCode::Home => target.scroll = 0,
KeyCode::Char('G') | KeyCode::End => {
target.scroll = target.lines.len().saturating_sub(1)
}
_ => {}
}
}
pub(super) fn key_logs(&mut self, key: KeyEvent) {
if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('s') {
self.save_logs();
return;
}
match key.code {
KeyCode::Char('s') | KeyCode::Char('f') => {
self.logs.follow = !self.logs.follow;
if self.logs.follow {
let overflow = self.logs.view.lines.len().saturating_sub(MAX_LOG_LINES);
if overflow > 0 {
self.logs.view.lines.drain(0..overflow);
}
}
self.flash = format!(
"autoscroll: {}",
if self.logs.follow { "on" } else { "off" }
);
self.flash_err = false;
return;
}
KeyCode::Char('w') => {
self.logs.wrap = !self.logs.wrap;
self.flash = format!("wrap: {}", if self.logs.wrap { "on" } else { "off" });
self.flash_err = false;
return;
}
KeyCode::Char('t') => {
self.logs.timestamps = !self.logs.timestamps;
self.flash = format!(
"timestamps: {}",
if self.logs.timestamps { "on" } else { "off" }
);
self.flash_err = false;
if !self.logs.stopped {
self.retail_logs();
}
return;
}
KeyCode::Char('x') => {
if self.logs.stopped {
self.logs.stopped = false;
self.flash = "log stream resumed".into();
self.flash_err = false;
self.retail_logs();
} else {
self.logs.stopped = true;
self.stop_log_stream(); self.flash = "log stream stopped (x to resume)".into();
self.flash_err = false;
}
return;
}
KeyCode::Char('c') => {
self.copy_logs();
return;
}
KeyCode::Char('/') => {
self.mode = Mode::LogFilter;
return;
}
_ => {}
}
let page = self.logs.viewport_h.max(1);
let max = self.logs.viewport_rows.saturating_sub(self.logs.viewport_h);
let cur = self.logs.view.scroll;
match key.code {
KeyCode::Esc | KeyCode::Char('q') => {
self.stop_log_stream();
self.mode = self.return_mode;
if self.return_mode == Mode::Table {
self.restore_selection();
}
}
KeyCode::Char('j') | KeyCode::Down => {
self.logs.follow = false;
self.logs.view.scroll = cur.saturating_add(1).min(max);
}
KeyCode::Char('k') | KeyCode::Up => {
self.logs.follow = false;
self.logs.view.scroll = cur.saturating_sub(1);
}
KeyCode::PageDown | KeyCode::Char(' ') => {
self.logs.follow = false;
self.logs.view.scroll = cur.saturating_add(page).min(max);
}
KeyCode::PageUp => {
self.logs.follow = false;
self.logs.view.scroll = cur.saturating_sub(page);
}
KeyCode::Char('g') | KeyCode::Home => {
self.logs.follow = false;
self.logs.view.scroll = 0;
}
KeyCode::Char('G') | KeyCode::End => {
self.logs.follow = true;
}
_ => {}
}
}
pub(super) fn key_log_filter(&mut self, key: KeyEvent) {
match key.code {
KeyCode::Esc => {
self.logs.filter.clear();
self.mode = Mode::Logs;
}
KeyCode::Enter => self.mode = Mode::Logs,
KeyCode::Backspace => {
self.logs.filter.pop();
}
KeyCode::Char(c) => self.logs.filter.push(c),
_ => {}
}
}
}