use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use super::catalog::Credential;
use super::state::{ConfirmPurpose, Edit, EditTarget, FieldValue, Step, Wizard};
use crate::tui::keymap;
use crate::tui::widgets::confirm::ConfirmOutcome;
use crate::tui::widgets::help::dismisses_help;
use crate::tui::widgets::line_edit::{EditOutcome, LineEdit};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Action {
Continue,
Save,
}
impl Wizard {
pub fn handle_key(&mut self, key: KeyEvent) -> Action {
if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
if self.confirm.is_some() || !self.dirty {
self.should_quit = true;
} else {
self.open_quit_confirm();
}
return Action::Continue;
}
if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('r') {
self.reveal = !self.reveal;
self.message = Some(if self.reveal {
"Credentials shown.".to_string()
} else {
"Credentials hidden.".to_string()
});
return Action::Continue;
}
if self.confirm.is_some() {
return self.handle_confirm_key(key);
}
if let Some(edit) = self.edit.take() {
self.handle_edit_key(key, edit);
return Action::Continue;
}
if self.show_help {
if dismisses_help(&key) {
self.show_help = false;
}
return Action::Continue;
}
self.handle_nav_key(key)
}
fn handle_confirm_key(&mut self, key: KeyEvent) -> Action {
let Some(mut pending) = self.confirm.take() else {
return Action::Continue;
};
match pending.dialog.handle(&key) {
ConfirmOutcome::Pending => {
self.confirm = Some(pending);
Action::Continue
}
ConfirmOutcome::No => Action::Continue,
ConfirmOutcome::Yes => match pending.purpose {
ConfirmPurpose::QuitDiscard => {
self.should_quit = true;
Action::Continue
}
ConfirmPurpose::SaveTos => {
self.claude_code_tos_accepted = true;
Action::Save
}
ConfirmPurpose::NoProviders => {
self.next_step();
Action::Continue
}
},
}
}
fn handle_edit_key(&mut self, key: KeyEvent, mut edit: Edit) {
match edit.line.handle_key(&key) {
EditOutcome::Commit => {
self.edit = Some(edit);
self.commit_edit();
self.message = None;
self.dirty = true;
}
EditOutcome::Cancel => {
self.message = Some("Edit cancelled.".to_string());
}
EditOutcome::Pending => self.edit = Some(edit),
}
}
fn handle_nav_key(&mut self, key: KeyEvent) -> Action {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
match key.code {
KeyCode::Char('s') if ctrl => return self.try_save(),
KeyCode::Char('o') => self.open_signup_page(),
KeyCode::Char('v') => self.verify_current(),
_ => match keymap::resolve(&key) {
Some(keymap::Action::Up) => self.move_cursor(-1),
Some(keymap::Action::Down) => self.move_cursor(1),
Some(keymap::Action::Left) => self.adjust(-1),
Some(keymap::Action::Right) => self.adjust(1),
Some(keymap::Action::Toggle) => self.toggle(),
Some(keymap::Action::Activate) => return self.activate(),
Some(keymap::Action::Back) | Some(keymap::Action::Prev) => self.back(),
Some(keymap::Action::Next) => self.forward_guarded(),
Some(keymap::Action::Help) => self.show_help = true,
Some(keymap::Action::Quit) => self.request_quit(),
Some(keymap::Action::ForceQuit) => self.should_quit = true,
None => {}
},
}
Action::Continue
}
fn request_quit(&mut self) {
if self.dirty {
self.open_quit_confirm();
} else {
self.should_quit = true;
}
}
fn try_save(&mut self) -> Action {
if self.needs_tos_confirmation() {
self.open_tos_confirm();
return Action::Continue;
}
Action::Save
}
fn activate(&mut self) -> Action {
if self.on_continue() {
return match self.step {
Step::Review => self.try_save(),
Step::Providers => {
self.forward_guarded();
Action::Continue
}
_ => {
self.forward();
Action::Continue
}
};
}
match self.step {
Step::Providers | Step::Agents | Step::Mcp => self.toggle(),
Step::ProviderDetail => {
if !self.open_credential_editor() {
self.adjust(1);
}
}
Step::Defaults | Step::Limits => self.activate_field(),
Step::Welcome | Step::Review => {}
}
Action::Continue
}
fn activate_field(&mut self) {
match self.fields().get(self.cursor).map(|f| &f.value) {
Some(FieldValue::Bool(_)) => self.toggle(),
Some(FieldValue::Choice { .. }) => self.adjust(1),
Some(FieldValue::Number(_)) => {
self.open_field_editor();
}
None => {}
}
}
fn open_credential_editor(&mut self) -> bool {
let Some((index, credential, value)) = self.detail_row().map(|index| {
let row = &self.providers[index];
(index, row.provider.credential, row.value.clone())
}) else {
return false;
};
if credential == Credential::None {
return false;
}
self.edit = Some(Edit {
target: EditTarget::Credential(index),
line: LineEdit::new(value, credential == Credential::ApiKey),
});
true
}
fn open_field_editor(&mut self) -> bool {
let cursor = self.cursor;
let Some(FieldValue::Number(current)) = self.fields().get(cursor).map(|f| &f.value) else {
return false;
};
let buffer = current.map(|n| n.to_string()).unwrap_or_default();
self.edit = Some(Edit {
target: EditTarget::Field(cursor),
line: LineEdit::new(buffer, false),
});
true
}
fn toggle(&mut self) {
match self.step {
Step::Providers => {
if let Some(row) = self.providers.get_mut(self.cursor) {
row.selected = !row.selected;
self.dirty = true;
if row.provider.id == "claude-code" && !row.selected {
self.claude_code_tos_accepted = false;
}
}
self.detail = 0;
}
Step::Agents => {
if let Some(row) = self.agents.get_mut(self.cursor) {
row.selected = !row.selected;
self.dirty = true;
}
}
Step::Mcp => {
if let Some(row) = self.mcp.get_mut(self.cursor) {
row.selected = !row.selected;
self.dirty = true;
}
}
Step::Defaults | Step::Limits => {
let cursor = self.cursor;
let mut changed = false;
if let Some(fields) = self.fields_mut()
&& let Some(field) = fields.get_mut(cursor)
&& let FieldValue::Bool(b) = &mut field.value
{
*b = !*b;
changed = true;
}
if changed {
self.dirty = true;
}
}
Step::Welcome | Step::ProviderDetail | Step::Review => {}
}
}
fn adjust(&mut self, delta: isize) {
match self.step {
Step::ProviderDetail => {
if let Some(index) = self.detail_row()
&& let Some(row) = self.providers.get_mut(index)
&& row.provider.credential == Credential::None
{
let count = super::state::effort_options().len();
let next = row.effort as isize + delta;
row.effort = next.rem_euclid(count as isize) as usize;
self.dirty = true;
}
}
Step::Defaults | Step::Limits => {
let cursor = self.cursor;
let mut changed_provider = false;
if let Some(fields) = self.fields_mut()
&& let Some(field) = fields.get_mut(cursor)
&& let FieldValue::Choice { options, index } = &mut field.value
&& !options.is_empty()
{
let next = *index as isize + delta;
*index = next.rem_euclid(options.len() as isize) as usize;
changed_provider = true;
}
if changed_provider {
self.dirty = true;
}
if changed_provider && self.step == Step::Defaults && cursor == 0 {
self.apply_provider_concurrency_default();
}
}
_ => {}
}
}
fn forward_guarded(&mut self) {
if self.step == Step::Providers && self.selected_providers().is_empty() {
self.open_no_providers_confirm();
return;
}
self.forward();
}
fn forward(&mut self) {
if self.step == Step::ProviderDetail {
if let Some(index) = self.detail_row() {
self.request_verification(index);
}
if self.next_detail() {
return;
}
}
self.next_step();
}
fn back(&mut self) {
if self.step == Step::ProviderDetail && self.prev_detail() {
return;
}
self.prev_step();
}
fn verify_current(&mut self) {
match self.step {
Step::ProviderDetail => {
if let Some(index) = self.detail_row() {
self.request_verification(index);
self.message = Some("Checking…".to_string());
}
}
Step::Providers | Step::Review => {
self.verify_all();
self.message = Some("Checking every selected provider…".to_string());
}
_ => {}
}
}
fn open_signup_page(&mut self) {
let url = match self.step {
Step::ProviderDetail => self
.detail_row()
.and_then(|i| self.providers.get(i))
.and_then(|r| r.provider.signup_url),
Step::Providers => self
.providers
.get(self.cursor)
.and_then(|r| r.provider.signup_url),
_ => None,
};
match url {
Some(url) => {
let opened = (self.opener)(url);
self.message = Some(if opened {
format!("Opened {url}")
} else {
format!("Couldn't open a browser. Visit {url}")
});
}
None => self.message = Some("Nothing to open here.".to_string()),
}
}
}
#[cfg(test)]
mod tests;