use uuid::Uuid;
use holodeck_core::models::Appearance;
use super::app_state::{AppState, CreateWizard, CreateWizardStep, Modal, PendingOperation};
use super::command_palette_reducer;
use super::event::{ReducerOutput, SideEffect};
use super::key::{Key, is_printable};
use super::launch_app_reducer;
use super::open_url_modal_reducer;
use super::privacy_wizard_reducer;
pub fn handle(state: &AppState, key: Key) -> ReducerOutput {
let mut next = state.clone();
match state.modal.clone() {
Some(Modal::Appearance(index)) => appearance(&next, index, key),
Some(Modal::ConfirmErase(id, index)) => {
confirm(&next, id, index, key, "Erasing…", PendingOperation::Erase, SideEffect::EraseSimulator(id))
}
Some(Modal::ConfirmDelete(id, index)) => {
confirm(&next, id, index, key, "Deleting…", PendingOperation::Delete, SideEffect::DeleteSimulator(id))
}
Some(Modal::CreateWizard(wizard)) => wizard_handle(&next, &wizard, key),
Some(Modal::PrivacyWizard(wizard)) => privacy_wizard_reducer::handle(&next, &wizard, key),
Some(Modal::Inspector(_)) => {
next.modal = None;
ReducerOutput::new(next)
}
Some(Modal::OpenUrl(prompt)) => open_url_modal_reducer::handle(&next, &prompt, key),
Some(Modal::LaunchApp(prompt)) => launch_app_reducer::handle(&next, &prompt, key),
Some(Modal::CommandPalette(palette)) => command_palette_reducer::handle(&next, &palette, key),
Some(Modal::Help) | None => {
next.modal = None;
ReducerOutput::new(next)
}
}
}
fn appearance(state: &AppState, index: i64, key: Key) -> ReducerOutput {
let mut next = state.clone();
match key {
Key::Up | Key::Char('k') => {
next.modal = Some(Modal::Appearance((index - 1).rem_euclid(2)));
ReducerOutput::new(next)
}
Key::Down | Key::Char('j') => {
next.modal = Some(Modal::Appearance((index + 1).rem_euclid(2)));
ReducerOutput::new(next)
}
Key::Enter | Key::Char(' ') => {
let appearance = if index == 0 { Appearance::Light } else { Appearance::Dark };
apply_appearance(&mut next, appearance)
}
Key::Char('l') => apply_appearance(&mut next, Appearance::Light),
Key::Char('d') => apply_appearance(&mut next, Appearance::Dark),
Key::Escape | Key::Char('q') => {
next.modal = None;
ReducerOutput::new(next)
}
_ => ReducerOutput::new(next),
}
}
fn apply_appearance(next: &mut AppState, appearance: Appearance) -> ReducerOutput {
let Some(sim) = next.selected_simulator().cloned() else {
next.modal = None;
return ReducerOutput::new(next.clone());
};
next.modal = None;
let label = if appearance == Appearance::Light { "light" } else { "dark" };
next.status_message = Some(format!("Setting appearance to {label}…"));
ReducerOutput::with_effects(next.clone(), vec![SideEffect::SetAppearance(sim.id, appearance)])
}
fn confirm(
state: &AppState,
id: Uuid,
index: i64,
key: Key,
status: &str,
operation: PendingOperation,
effect: SideEffect,
) -> ReducerOutput {
let mut next = state.clone();
let with_index = |idx: i64| match &state.modal {
Some(Modal::ConfirmErase(uid, _)) => Modal::ConfirmErase(*uid, idx),
_ => Modal::ConfirmDelete(id, idx),
};
match key {
Key::Up | Key::Char('k') => {
next.modal = Some(with_index((index - 1).rem_euclid(2)));
ReducerOutput::new(next)
}
Key::Down | Key::Char('j') => {
next.modal = Some(with_index((index + 1).rem_euclid(2)));
ReducerOutput::new(next)
}
Key::Enter | Key::Char(' ') => {
if index == 0 {
execute_confirm(next, id, status, operation, effect)
} else {
next.modal = None;
ReducerOutput::new(next)
}
}
Key::Char('y') | Key::Char('Y') => execute_confirm(next, id, status, operation, effect),
Key::Char('n') | Key::Char('N') | Key::Escape | Key::Char('q') => {
next.modal = None;
ReducerOutput::new(next)
}
_ => ReducerOutput::new(next),
}
}
fn execute_confirm(mut next: AppState, id: Uuid, status: &str, operation: PendingOperation, effect: SideEffect) -> ReducerOutput {
next.modal = None;
if next.pending_operations.contains_key(&id) {
next.status_message = Some("Simulator already has a pending operation".to_string());
return ReducerOutput::new(next);
}
next.status_message = Some(status.to_string());
next.pending_operations.insert(id, operation);
ReducerOutput::with_effects(next, vec![effect])
}
fn wizard_handle(state: &AppState, wizard: &CreateWizard, key: Key) -> ReducerOutput {
let mut next = state.clone();
let mut updated = wizard.clone();
if let Key::Escape = key {
let filter_is_live = wizard.is_device_type_filter_focused || !wizard.device_type_filter.is_empty();
if wizard.step == CreateWizardStep::PickDeviceType && filter_is_live {
updated.is_device_type_filter_focused = false;
updated.device_type_filter.clear();
updated.device_type_index = 0;
updated.device_type_scroll_offset = 0;
next.modal = Some(Modal::CreateWizard(updated));
return ReducerOutput::new(next);
}
next.modal = None;
return ReducerOutput::new(next);
}
let viewport = CreateWizard::viewport(state.rows);
match wizard.step {
CreateWizardStep::Loading => return ReducerOutput::new(next),
CreateWizardStep::PickDeviceType => {
if wizard.is_device_type_filter_focused {
match key {
Key::Enter => updated.is_device_type_filter_focused = false,
Key::Backspace => {
if !updated.device_type_filter.is_empty() {
updated.device_type_filter.pop();
}
updated.device_type_index = 0;
updated.device_type_scroll_offset = 0;
}
Key::Char(c) if is_printable(c) => {
updated.device_type_filter.push(c);
updated.device_type_index = 0;
updated.device_type_scroll_offset = 0;
}
_ => {}
}
next.modal = Some(Modal::CreateWizard(updated));
return ReducerOutput::new(next);
}
match key {
Key::Up | Key::Char('k') => updated.device_type_index = (updated.device_type_index - 1).max(0),
Key::Down | Key::Char('j') => {
let last_index = (updated.visible_device_types().len() as i64 - 1).max(0);
updated.device_type_index = (updated.device_type_index + 1).min(last_index);
}
Key::Char('/') => {
updated.is_device_type_filter_focused = true;
if updated.device_type_filter.is_empty() {
updated.device_type_index = 0;
updated.device_type_scroll_offset = 0;
}
}
Key::Enter => {
if updated.selected_device_type().is_none() {
return ReducerOutput::new(next);
}
updated.step = CreateWizardStep::PickRuntime;
}
_ => {}
}
let device_type_viewport = updated.device_type_viewport(state.rows);
updated.device_type_scroll_offset =
AppState::scroll(updated.device_type_scroll_offset, updated.device_type_index, device_type_viewport);
}
CreateWizardStep::PickRuntime => {
match key {
Key::Up | Key::Char('k') => updated.runtime_index = (updated.runtime_index - 1).max(0),
Key::Down | Key::Char('j') => {
updated.runtime_index = (updated.runtime_index + 1).min((updated.runtimes.len() as i64 - 1).max(0));
}
Key::Enter => {
if updated.selected_runtime().is_none() {
return ReducerOutput::new(next);
}
updated.step = CreateWizardStep::Confirm;
}
Key::Char('b') => updated.step = CreateWizardStep::PickDeviceType,
_ => {}
}
updated.runtime_scroll_offset = AppState::scroll(updated.runtime_scroll_offset, updated.runtime_index, viewport);
}
CreateWizardStep::Confirm => match key {
Key::Enter | Key::Char('y') => {
let (Some(device_type), Some(runtime)) =
(updated.selected_device_type().cloned(), updated.selected_runtime().cloned())
else {
return ReducerOutput::new(next);
};
updated.step = CreateWizardStep::Submitting;
updated.error = None;
let effect = SideEffect::CreateSimulator { name: updated.default_name(), device_type, runtime };
next.modal = Some(Modal::CreateWizard(updated));
return ReducerOutput::with_effects(next, vec![effect]);
}
Key::Char('b') => {
updated.step = CreateWizardStep::PickRuntime;
updated.error = None;
}
_ => {}
},
CreateWizardStep::Submitting => return ReducerOutput::new(next),
}
next.modal = Some(Modal::CreateWizard(updated));
ReducerOutput::new(next)
}