leviath_cli/commands/setup/
input.rs1use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
17
18use super::catalog::Credential;
19use super::state::{ConfirmPurpose, Edit, EditTarget, FieldValue, Step, Wizard};
20use crate::tui::keymap;
21use crate::tui::widgets::confirm::ConfirmOutcome;
22use crate::tui::widgets::help::dismisses_help;
23use crate::tui::widgets::line_edit::{EditOutcome, LineEdit};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum Action {
28 Continue,
30 Save,
32}
33
34impl Wizard {
35 pub fn handle_key(&mut self, key: KeyEvent) -> Action {
37 if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
42 if self.confirm.is_some() || !self.dirty {
43 self.should_quit = true;
44 } else {
45 self.open_quit_confirm();
46 }
47 return Action::Continue;
48 }
49 if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('r') {
52 self.reveal = !self.reveal;
53 self.message = Some(if self.reveal {
54 "Credentials shown.".to_string()
55 } else {
56 "Credentials hidden.".to_string()
57 });
58 return Action::Continue;
59 }
60 if self.confirm.is_some() {
61 return self.handle_confirm_key(key);
62 }
63 if let Some(edit) = self.edit.take() {
64 self.handle_edit_key(key, edit);
65 return Action::Continue;
66 }
67 if self.show_help {
68 if dismisses_help(&key) {
69 self.show_help = false;
70 }
71 return Action::Continue;
72 }
73 self.handle_nav_key(key)
74 }
75
76 fn handle_confirm_key(&mut self, key: KeyEvent) -> Action {
79 let Some(mut pending) = self.confirm.take() else {
80 return Action::Continue;
81 };
82 match pending.dialog.handle(&key) {
83 ConfirmOutcome::Pending => {
84 self.confirm = Some(pending);
85 Action::Continue
86 }
87 ConfirmOutcome::No => Action::Continue,
88 ConfirmOutcome::Yes => match pending.purpose {
89 ConfirmPurpose::QuitDiscard => {
90 self.should_quit = true;
91 Action::Continue
92 }
93 ConfirmPurpose::SaveTos => {
94 self.claude_code_tos_accepted = true;
95 Action::Save
96 }
97 ConfirmPurpose::NoProviders => {
98 self.next_step();
99 Action::Continue
100 }
101 },
102 }
103 }
104
105 fn handle_edit_key(&mut self, key: KeyEvent, mut edit: Edit) {
111 match edit.line.handle_key(&key) {
112 EditOutcome::Commit => {
113 self.edit = Some(edit);
114 self.commit_edit();
115 self.message = None;
116 self.dirty = true;
117 }
118 EditOutcome::Cancel => {
119 self.message = Some("Edit cancelled.".to_string());
120 }
121 EditOutcome::Pending => self.edit = Some(edit),
122 }
123 }
124
125 fn handle_nav_key(&mut self, key: KeyEvent) -> Action {
128 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
129 match key.code {
130 KeyCode::Char('s') if ctrl => return self.try_save(),
131 KeyCode::Char('o') => self.open_signup_page(),
132 KeyCode::Char('v') => self.verify_current(),
133 _ => match keymap::resolve(&key) {
134 Some(keymap::Action::Up) => self.move_cursor(-1),
135 Some(keymap::Action::Down) => self.move_cursor(1),
136 Some(keymap::Action::Left) => self.adjust(-1),
137 Some(keymap::Action::Right) => self.adjust(1),
138 Some(keymap::Action::Toggle) => self.toggle(),
139 Some(keymap::Action::Activate) => return self.activate(),
140 Some(keymap::Action::Back) | Some(keymap::Action::Prev) => self.back(),
141 Some(keymap::Action::Next) => self.forward_guarded(),
142 Some(keymap::Action::Help) => self.show_help = true,
143 Some(keymap::Action::Quit) => self.request_quit(),
144 Some(keymap::Action::ForceQuit) => self.should_quit = true,
147 None => {}
148 },
149 }
150 Action::Continue
151 }
152
153 fn request_quit(&mut self) {
155 if self.dirty {
156 self.open_quit_confirm();
157 } else {
158 self.should_quit = true;
159 }
160 }
161
162 fn try_save(&mut self) -> Action {
164 if self.needs_tos_confirmation() {
165 self.open_tos_confirm();
166 return Action::Continue;
167 }
168 Action::Save
169 }
170
171 fn activate(&mut self) -> Action {
174 if self.on_continue() {
175 return match self.step {
176 Step::Review => self.try_save(),
177 Step::Providers => {
178 self.forward_guarded();
179 Action::Continue
180 }
181 _ => {
182 self.forward();
183 Action::Continue
184 }
185 };
186 }
187 match self.step {
188 Step::Providers | Step::Agents | Step::Mcp => self.toggle(),
189 Step::ProviderDetail => {
190 if !self.open_credential_editor() {
193 self.adjust(1);
194 }
195 }
196 Step::Defaults | Step::Limits => self.activate_field(),
197 Step::Welcome | Step::Review => {}
201 }
202 Action::Continue
203 }
204
205 fn activate_field(&mut self) {
208 match self.fields().get(self.cursor).map(|f| &f.value) {
209 Some(FieldValue::Bool(_)) => self.toggle(),
210 Some(FieldValue::Choice { .. }) => self.adjust(1),
211 Some(FieldValue::Number(_)) => {
212 self.open_field_editor();
213 }
214 None => {}
216 }
217 }
218
219 fn open_credential_editor(&mut self) -> bool {
222 let Some((index, credential, value)) = self.detail_row().map(|index| {
223 let row = &self.providers[index];
224 (index, row.provider.credential, row.value.clone())
225 }) else {
226 return false;
227 };
228 if credential == Credential::None {
229 return false;
230 }
231 self.edit = Some(Edit {
232 target: EditTarget::Credential(index),
233 line: LineEdit::new(value, credential == Credential::ApiKey),
234 });
235 true
236 }
237
238 fn open_field_editor(&mut self) -> bool {
241 let cursor = self.cursor;
242 let Some(FieldValue::Number(current)) = self.fields().get(cursor).map(|f| &f.value) else {
243 return false;
244 };
245 let buffer = current.map(|n| n.to_string()).unwrap_or_default();
246 self.edit = Some(Edit {
247 target: EditTarget::Field(cursor),
248 line: LineEdit::new(buffer, false),
249 });
250 true
251 }
252
253 fn toggle(&mut self) {
255 match self.step {
256 Step::Providers => {
257 if let Some(row) = self.providers.get_mut(self.cursor) {
258 row.selected = !row.selected;
259 self.dirty = true;
260 if row.provider.id == "claude-code" && !row.selected {
264 self.claude_code_tos_accepted = false;
265 }
266 }
267 self.detail = 0;
271 }
272 Step::Agents => {
273 if let Some(row) = self.agents.get_mut(self.cursor) {
274 row.selected = !row.selected;
275 self.dirty = true;
276 }
277 }
278 Step::Mcp => {
279 if let Some(row) = self.mcp.get_mut(self.cursor) {
280 row.selected = !row.selected;
281 self.dirty = true;
282 }
283 }
284 Step::Defaults | Step::Limits => {
285 let cursor = self.cursor;
286 let mut changed = false;
287 if let Some(fields) = self.fields_mut()
288 && let Some(field) = fields.get_mut(cursor)
289 && let FieldValue::Bool(b) = &mut field.value
290 {
291 *b = !*b;
292 changed = true;
293 }
294 if changed {
295 self.dirty = true;
296 }
297 }
298 Step::Welcome | Step::ProviderDetail | Step::Review => {}
299 }
300 }
301
302 fn adjust(&mut self, delta: isize) {
305 match self.step {
306 Step::ProviderDetail => {
307 if let Some(index) = self.detail_row()
309 && let Some(row) = self.providers.get_mut(index)
310 && row.provider.credential == Credential::None
311 {
312 let count = super::state::effort_options().len();
313 let next = row.effort as isize + delta;
314 row.effort = next.rem_euclid(count as isize) as usize;
315 self.dirty = true;
316 }
317 }
318 Step::Defaults | Step::Limits => {
319 let cursor = self.cursor;
320 let mut changed_provider = false;
321 if let Some(fields) = self.fields_mut()
322 && let Some(field) = fields.get_mut(cursor)
323 && let FieldValue::Choice { options, index } = &mut field.value
324 && !options.is_empty()
325 {
326 let next = *index as isize + delta;
327 *index = next.rem_euclid(options.len() as isize) as usize;
328 changed_provider = true;
329 }
330 if changed_provider {
331 self.dirty = true;
332 }
333 if changed_provider && self.step == Step::Defaults && cursor == 0 {
337 self.apply_provider_concurrency_default();
338 }
339 }
340 _ => {}
341 }
342 }
343
344 fn forward_guarded(&mut self) {
347 if self.step == Step::Providers && self.selected_providers().is_empty() {
348 self.open_no_providers_confirm();
349 return;
350 }
351 self.forward();
352 }
353
354 fn forward(&mut self) {
356 if self.step == Step::ProviderDetail {
357 if let Some(index) = self.detail_row() {
360 self.request_verification(index);
361 }
362 if self.next_detail() {
363 return;
364 }
365 }
366 self.next_step();
367 }
368
369 fn back(&mut self) {
371 if self.step == Step::ProviderDetail && self.prev_detail() {
372 return;
373 }
374 self.prev_step();
375 }
376
377 fn verify_current(&mut self) {
379 match self.step {
380 Step::ProviderDetail => {
381 if let Some(index) = self.detail_row() {
382 self.request_verification(index);
383 self.message = Some("Checking…".to_string());
384 }
385 }
386 Step::Providers | Step::Review => {
387 self.verify_all();
388 self.message = Some("Checking every selected provider…".to_string());
389 }
390 _ => {}
391 }
392 }
393
394 fn open_signup_page(&mut self) {
400 let url = match self.step {
401 Step::ProviderDetail => self
402 .detail_row()
403 .and_then(|i| self.providers.get(i))
404 .and_then(|r| r.provider.signup_url),
405 Step::Providers => self
406 .providers
407 .get(self.cursor)
408 .and_then(|r| r.provider.signup_url),
409 _ => None,
410 };
411 match url {
412 Some(url) => {
413 let opened = (self.opener)(url);
414 self.message = Some(if opened {
415 format!("Opened {url}")
416 } else {
417 format!("Couldn't open a browser. Visit {url}")
418 });
419 }
420 None => self.message = Some("Nothing to open here.".to_string()),
421 }
422 }
423}
424
425#[cfg(test)]
426mod tests;