use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use std::path::PathBuf;
use crate::manifest::Manifest;
use super::overlay::ConfirmAction;
use super::overlay::ExportKind;
use super::*;
impl App {
pub(super) fn on_key(&mut self, key: KeyEvent) {
if self.overlay.is_some() {
let mut close = false;
let mut confirm: Option<ConfirmAction> = None;
let mut do_export: Option<(String, ExportKind)> = None;
let mut yank: Option<String> = None;
let mut reopen: Option<(String, Option<PathBuf>, bool)> = None;
let mut discarded = false;
match self.overlay.as_mut().unwrap() {
Overlay::Text { scroll, body, .. } => match key.code {
KeyCode::Down | KeyCode::Char('j') => *scroll = scroll.saturating_add(1),
KeyCode::Up | KeyCode::Char('k') => *scroll = scroll.saturating_sub(1),
KeyCode::Char('y') => yank = Some(body.clone()),
_ => close = true,
},
Overlay::Qr { .. } => close = true,
Overlay::Confirm { action, .. } => {
if matches!(
key.code,
KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Enter
) {
confirm = Some(action.clone());
}
close = true; }
Overlay::Menu {
name, items, idx, ..
} => match key.code {
KeyCode::Down | KeyCode::Char('j') => *idx = (*idx + 1) % items.len(),
KeyCode::Up | KeyCode::Char('k') => {
*idx = (*idx + items.len() - 1) % items.len()
}
KeyCode::Enter => {
do_export = Some((name.clone(), items[*idx].1));
close = true;
}
KeyCode::Esc | KeyCode::Char('q') => close = true,
_ => {}
},
Overlay::Invalid {
content,
original,
was_up,
..
} => {
match key.code {
KeyCode::Char('e') | KeyCode::Char('c') | KeyCode::Enter => {
reopen = Some((content.clone(), original.clone(), *was_up))
}
_ => discarded = true, }
close = true;
}
}
if discarded {
self.set_status("discarded");
}
if let Some((content, original, was_up)) = reopen {
self.overlay = None;
self.reopen_editor(content, original, was_up);
return;
}
if let Some(text) = yank {
match copy_clipboard(&text) {
Some(tool) => self.set_status(format!("copied to clipboard ({tool})")),
None => self.set_status("no clipboard tool - install wl-clipboard or xclip"),
}
} else if let Some(action) = confirm {
self.overlay = None;
match action {
ConfirmAction::DeleteNode(name) => self.delete_node(&name),
ConfirmAction::DeleteIface(path) => self.delete_iface(path),
}
} else if let Some((name, kind)) = do_export {
self.overlay = None;
self.export(&name, kind);
} else if close {
self.overlay = None;
}
return;
}
if self.prompt.is_some() {
self.prompt_key(key);
return;
}
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
if ctrl && key.code == KeyCode::Char('c') {
self.should_quit = true;
return;
}
match key.code {
KeyCode::Char('q') | KeyCode::Esc => self.should_quit = true,
KeyCode::Tab | KeyCode::Right | KeyCode::Char('l') => self.cycle_view(1),
KeyCode::BackTab | KeyCode::Left | KeyCode::Char('h') => self.cycle_view(-1),
KeyCode::Down | KeyCode::Char('j') => self.move_sel(1),
KeyCode::Up | KeyCode::Char('k') => self.move_sel(-1),
_ => match self.view {
View::Interfaces => self.interfaces_key(key),
View::Mesh => self.mesh_key(key),
},
}
}
pub(super) fn interfaces_key(&mut self, key: KeyEvent) {
match key.code {
KeyCode::Enter => {
let sel = self.selected_iface();
let (path, up) = match sel {
Some(i) => (Some(i.path.clone()), !i.up),
None => (None, false),
};
let m = act(path, up);
self.reload(m);
}
KeyCode::Char('c') => self.start_create_conf(),
KeyCode::Char('e') => self.start_edit_conf(),
KeyCode::Char('d') => self.confirm_delete_iface(),
KeyCode::Char('b') => self.toggle_boot(),
KeyCode::Char('i') => self.inspect_iface(),
KeyCode::Char('r') => self.reload("refreshed"),
_ => {}
}
}
pub(super) fn mesh_key(&mut self, key: KeyEvent) {
match key.code {
KeyCode::Char('c') => {
self.prompt = Some(Prompt::create_node(
NodeKind::Spoke,
KeySource::Generate,
&self.next_address(),
&self.hub_names(),
))
}
KeyCode::Char('e') => {
let hubs = self.hub_names();
let Some(node) = self.selected_mesh_node() else {
self.set_status("no node selected");
return;
};
self.prompt = Some(Prompt::edit_node(node, &hubs));
}
KeyCode::Char('R') => self.rotate_selected(),
KeyCode::Char('E') => self.open_export(),
KeyCode::Char('i') => {
let Some(name) = self.selected_mesh_node().map(|n| n.name.clone()) else {
self.set_status("no node selected");
return;
};
match Manifest::load_or_empty(&self.manifest_path) {
Ok(m) => match m.nodes.iter().find(|n| n.name == name) {
Some(node) => {
self.overlay = Some(Overlay::Text {
title: format!(" {name}.conf (generated) "),
body: m.node_config(node),
scroll: 0,
})
}
None => self.set_status("node vanished"),
},
Err(e) => self.set_status(format!("couldn't load manifest: {e}")),
}
}
KeyCode::Char('d') => {
let Some(name) = self.selected_mesh_node().map(|n| n.name.clone()) else {
self.set_status("no node selected");
return;
};
self.overlay = Some(Overlay::Confirm {
prompt: format!(" delete `{name}` from mesh.toml? y / n "),
action: ConfirmAction::DeleteNode(name),
});
}
KeyCode::Enter => {
let Some(name) = self.selected_mesh_node().map(|n| n.name.clone()) else {
self.set_status("no node selected");
return;
};
match Manifest::load_or_empty(&self.manifest_path) {
Ok(m) => self.show_qr(&m, name),
Err(e) => self.set_status(format!("couldn't load manifest: {e}")),
}
}
KeyCode::Char('g') => match self.gen_all() {
Ok(n) => self.set_status(format!("wrote {n} configs to ./out")),
Err(e) => self.set_status(format!("gen failed: {e}")),
},
KeyCode::Char('r') => self.reload("reloaded manifest"),
_ => {}
}
}
pub(super) fn prompt_key(&mut self, key: KeyEvent) {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
let len = self.prompt.as_ref().map(|p| p.fields.len()).unwrap_or(0);
if len == 0 {
return;
}
let on_choice = self
.prompt
.as_ref()
.map(|p| p.fields[p.idx].is_choice())
.unwrap_or(false);
if on_choice {
let delta = match key.code {
KeyCode::Right | KeyCode::Char('l') => 1,
KeyCode::Left | KeyCode::Char('h') => -1,
_ => 0,
};
if delta != 0 {
let p = self.prompt.as_ref().unwrap();
let cur_kind = matches!(p.fields[p.idx].kind, FieldKind::Type(_));
let cur_key = matches!(p.fields[p.idx].kind, FieldKind::Key(_));
let (addr, hubs) = (self.next_address(), self.hub_names());
let p = self.prompt.as_mut().unwrap();
if cur_kind {
p.toggle_kind(&addr, &hubs);
} else if cur_key {
p.toggle_keysrc(&addr, &hubs);
} else {
p.cycle_pick(delta);
}
return;
}
}
let next = matches!(key.code, KeyCode::Tab | KeyCode::Down)
|| (ctrl && key.code == KeyCode::Char('j'));
let prev = matches!(key.code, KeyCode::BackTab | KeyCode::Up)
|| (ctrl && key.code == KeyCode::Char('k'));
if next {
let p = self.prompt.as_mut().unwrap();
p.idx = (p.idx + 1) % len;
return;
}
if prev {
let p = self.prompt.as_mut().unwrap();
p.idx = (p.idx + len - 1) % len;
return;
}
match key.code {
KeyCode::Esc => {
self.prompt = None;
self.set_status("cancelled");
}
KeyCode::Char('c') if ctrl => {
self.prompt = None;
self.set_status("cancelled");
}
KeyCode::Enter => {
let idx = self.prompt.as_ref().unwrap().idx;
if idx + 1 < len {
self.prompt.as_mut().unwrap().idx += 1;
} else {
self.submit_prompt();
}
}
KeyCode::Backspace => {
self.prompt.as_mut().unwrap().cur_mut().value.pop();
}
KeyCode::Char(c) if !ctrl => {
self.prompt.as_mut().unwrap().cur_mut().value.push(c);
}
_ => {}
}
}
}