1use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
4use horto_os_ui_shared::SetupKind;
5use std::sync::atomic::Ordering;
6
7use crate::app::{App, Modal};
8use crate::prompt::{confirm_key, ConfirmResult, TextInputResult};
9use crate::tabs::Screen;
10
11pub fn is_quit(key: KeyEvent) -> bool {
12 matches!(key.code, KeyCode::Char('q' | 'Q'))
13 || (key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL))
14}
15
16pub fn ctrl_c_quit(key: KeyEvent) -> bool {
17 key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL)
18}
19
20pub fn handle_screen_key(app: &mut App, key: KeyEvent) {
21 match key.code {
22 KeyCode::Char('?') => {
23 app.help_open = true;
24 app.message = "Help".into();
25 }
26 KeyCode::Tab => {
27 app.apply = !app.apply;
28 app.message = if app.apply {
29 "APPLY".into()
30 } else {
31 "PLAN".into()
32 };
33 if app.remote.is_none() {
34 app.refresh();
35 }
36 }
37 KeyCode::Char(d) if d.is_ascii_digit() => {
38 if let Some(screen) = Screen::from_digit(d, app.is_remote()) {
39 app.select_screen(screen);
40 }
41 }
42 KeyCode::Char('c') if app.screen == Screen::Logs => app.clear_logs(),
43 KeyCode::Char('r') => {
44 app.refresh();
45 if app.remote.is_none() {
46 app.message = "Refreshed".into();
47 }
48 }
49 KeyCode::Char('f' | 'F') => {
50 app.fetch_current_tab();
51 }
52 KeyCode::Char('a') => app.run_all(),
53 KeyCode::Char('b') => app.run_backup_etc(),
54 KeyCode::Char('B') => app.show_disk_backup_status(),
55 KeyCode::Char('p' | 'P') => {
56 let next = if app.kind == SetupKind::Full {
57 SetupKind::Minimal
58 } else {
59 SetupKind::Full
60 };
61 app.set_pipeline_kind_local(next);
62 }
63 KeyCode::Char('e' | 'E') if app.screen == Screen::Ssh => {
64 app.open_host_editor();
65 }
66 KeyCode::Char('i' | 'I') if app.screen == Screen::Ssh => {
67 app.install_ssh_key_action();
68 }
69 KeyCode::Enter if app.screen == Screen::Setup => {
70 app.run_selected();
71 }
72 KeyCode::Enter
73 if matches!(
74 app.screen,
75 Screen::Ssh
76 | Screen::Cli
77 | Screen::Api
78 | Screen::Mcp
79 | Screen::Overview
80 | Screen::Reboot
81 ) =>
82 {
83 app.run_surface_enter();
84 }
85 KeyCode::Up | KeyCode::Char('k') => {
86 if let Some(i) = app.step_state.selected() {
87 if i > 0 {
88 app.step_state.select(Some(i - 1));
89 }
90 }
91 }
92 KeyCode::Down | KeyCode::Char('j') => {
93 let len = app.status_lines.len();
94 if let Some(i) = app.step_state.selected() {
95 if i + 1 < len {
96 app.step_state.select(Some(i + 1));
97 }
98 }
99 }
100 KeyCode::Left => {
101 app.prev_screen();
102 }
103 KeyCode::Right => {
104 app.next_screen();
105 }
106 _ => {}
107 }
108}
109
110pub fn handle_modal_key(app: &mut App, key: KeyEvent) -> bool {
112 match app.modal {
113 Some(Modal::TextHost(_)) => {
114 let Some(Modal::TextHost(mut input)) = app.modal.take() else {
115 return true;
116 };
117 match input.handle_key(key) {
118 TextInputResult::Continue => {
119 app.modal = Some(Modal::TextHost(input));
120 }
121 TextInputResult::Submit(value) => {
122 app.apply_host_edit(&value);
123 }
124 TextInputResult::Cancel => {
125 app.push_log("Host edit cancelled");
126 }
127 }
128 true
129 }
130 Some(Modal::SudoPassword(_)) => {
131 let Some(Modal::SudoPassword(mut input)) = app.modal.take() else {
132 return true;
133 };
134 match input.handle_key(key) {
135 TextInputResult::Continue => {
136 app.modal = Some(Modal::SudoPassword(input));
137 }
138 TextInputResult::Submit(password) => {
139 if password.is_empty() {
140 app.push_log("Reboot cancelled (empty password)");
141 app.note("Reboot cancelled");
142 } else {
143 app.do_reboot(password);
144 }
145 }
146 TextInputResult::Cancel => {
147 app.push_log("Reboot cancelled");
148 app.note("Reboot cancelled");
149 }
150 }
151 true
152 }
153 Some(Modal::Rebooting) => {
154 if key.code == KeyCode::Esc {
155 app.reboot_cancel.store(true, Ordering::SeqCst);
156 app.reboot_inflight = false;
157 app.reboot_rx = None;
158 app.modal = None;
159 }
160 true
161 }
162 Some(Modal::Confirm(_)) => {
163 match confirm_key(key) {
164 ConfirmResult::Yes => app.resolve_confirm_yes(),
165 ConfirmResult::No => app.resolve_confirm_no(),
166 ConfirmResult::Ignore => {}
167 }
168 true
169 }
170 None => false,
171 }
172}