use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use std::sync::atomic::Ordering; use super::{InputContext, InputResult}; use crate::commands::AppCommand; use crate::state::{ ConfirmAction, CyclicEnum, EuclideanField, Modal, PanelFocus, PatternField, RenameTarget, SampleBrowserState, SidePanel, }; pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputResult { let shift = key.modifiers.contains(KeyModifiers::SHIFT); match key.code { KeyCode::Tab => { if ctx.app.panel.visible { ctx.app.panel.visible = false; ctx.app.panel.focus = PanelFocus::Main; } else { if ctx.app.panel.side.is_none() { let state = SampleBrowserState::new(&ctx.app.audio.config.sample_paths); ctx.app.panel.side = Some(SidePanel::SampleBrowser(state)); } ctx.app.panel.visible = true; ctx.app.panel.focus = PanelFocus::Side; } } KeyCode::Char('q') => { ctx.dispatch(AppCommand::OpenModal(Modal::Confirm { action: ConfirmAction::Quit, selected: false, })); } KeyCode::Char(' ') => { ctx.dispatch(AppCommand::TogglePlaying); ctx.playing .store(ctx.app.playback.playing, Ordering::Relaxed); } KeyCode::Left if shift && !ctrl => { if ctx.app.editor_ctx.selection_anchor.is_none() { ctx.dispatch(AppCommand::SetSelectionAnchor(ctx.app.editor_ctx.step)); } ctx.dispatch(AppCommand::PrevStep); } KeyCode::Right if shift && !ctrl => { if ctx.app.editor_ctx.selection_anchor.is_none() { ctx.dispatch(AppCommand::SetSelectionAnchor(ctx.app.editor_ctx.step)); } ctx.dispatch(AppCommand::NextStep); } KeyCode::Up if shift && !ctrl => { if ctx.app.editor_ctx.selection_anchor.is_none() { ctx.dispatch(AppCommand::SetSelectionAnchor(ctx.app.editor_ctx.step)); } ctx.dispatch(AppCommand::StepUp); } KeyCode::Down if shift && !ctrl => { if ctx.app.editor_ctx.selection_anchor.is_none() { ctx.dispatch(AppCommand::SetSelectionAnchor(ctx.app.editor_ctx.step)); } ctx.dispatch(AppCommand::StepDown); } KeyCode::Left => { ctx.app.editor_ctx.clear_selection(); ctx.dispatch(AppCommand::PrevStep); } KeyCode::Right => { ctx.app.editor_ctx.clear_selection(); ctx.dispatch(AppCommand::NextStep); } KeyCode::Up => { ctx.app.editor_ctx.clear_selection(); ctx.dispatch(AppCommand::StepUp); } KeyCode::Down => { ctx.app.editor_ctx.clear_selection(); ctx.dispatch(AppCommand::StepDown); } KeyCode::Esc => { ctx.app.editor_ctx.clear_selection(); } KeyCode::Enter => { ctx.app.editor_ctx.clear_selection(); ctx.dispatch(AppCommand::OpenModal(Modal::Editor)); } KeyCode::Char('t') => ctx.dispatch(AppCommand::ToggleSteps), KeyCode::Char('s') => { use crate::state::file_browser::FileBrowserState; let initial = ctx .app .project_state .file_path .as_ref() .map(|p| p.display().to_string()) .unwrap_or_default(); let state = FileBrowserState::new_save(initial); ctx.dispatch(AppCommand::OpenModal(Modal::FileBrowser(Box::new(state)))); } KeyCode::Char('c') if ctrl => { ctx.dispatch(AppCommand::CopySteps); } KeyCode::Char('v') if ctrl => { ctx.dispatch(AppCommand::PasteSteps); } KeyCode::Char('b') if ctrl => { ctx.dispatch(AppCommand::LinkPasteSteps); } KeyCode::Char('d') if ctrl => { ctx.dispatch(AppCommand::DuplicateSteps); } KeyCode::Char('h') if ctrl => ctx.dispatch(AppCommand::HardenSteps), KeyCode::Char('l') => { use crate::state::file_browser::FileBrowserState; let default_dir = ctx .app .project_state .file_path .as_ref() .and_then(|p| p.parent()) .map(|p| { let mut s = p.display().to_string(); if !s.ends_with('/') { s.push('/'); } s }) .unwrap_or_default(); let state = FileBrowserState::new_load(default_dir); ctx.dispatch(AppCommand::OpenModal(Modal::FileBrowser(Box::new(state)))); } KeyCode::Char('+') | KeyCode::Char('=') => ctx.dispatch(AppCommand::TempoUp), KeyCode::Char('-') => ctx.dispatch(AppCommand::TempoDown), KeyCode::Char('T') => { let current = format!("{:.1}", ctx.link.tempo()); ctx.dispatch(AppCommand::OpenModal(Modal::SetTempo(current))); } KeyCode::Char('<') | KeyCode::Char(',') => ctx.dispatch(AppCommand::LengthDecrease), KeyCode::Char('>') | KeyCode::Char('.') => ctx.dispatch(AppCommand::LengthIncrease), KeyCode::Char('[') => ctx.dispatch(AppCommand::SpeedDecrease), KeyCode::Char(']') => ctx.dispatch(AppCommand::SpeedIncrease), KeyCode::Char('L') => ctx.dispatch(AppCommand::OpenPatternModal(PatternField::Length)), KeyCode::Char('S') => ctx.dispatch(AppCommand::OpenPatternModal(PatternField::Speed)), KeyCode::Char('p') => ctx.dispatch(AppCommand::OpenModal(Modal::Preview)), KeyCode::Delete | KeyCode::Backspace => { let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern); if let Some(range) = ctx.app.editor_ctx.selection_range() { let steps: Vec = range.collect(); ctx.dispatch(AppCommand::OpenModal(Modal::Confirm { action: ConfirmAction::DeleteSteps { bank, pattern, steps }, selected: false, })); } else { let step = ctx.app.editor_ctx.step; ctx.dispatch(AppCommand::OpenModal(Modal::Confirm { action: ConfirmAction::DeleteStep { bank, pattern, step }, selected: false, })); } } KeyCode::Char('r') if ctrl => { let pattern = ctx.app.current_edit_pattern(); if let Some(script) = pattern.resolve_script(ctx.app.editor_ctx.step) { if !script.trim().is_empty() { match ctx .app .execute_script_oneshot(script, ctx.link, ctx.audio_tx) { Ok(()) => ctx .app .ui .flash("Executed", 100, crate::state::FlashKind::Info), Err(e) => ctx.app.ui.flash( &format!("Error: {e}"), 200, crate::state::FlashKind::Error, ), } } } } KeyCode::Char('r') => { let (bank, pattern, step) = ( ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern, ctx.app.editor_ctx.step, ); let current_name = ctx .app .current_edit_pattern() .step(step) .and_then(|s| s.name.clone()) .unwrap_or_default(); ctx.dispatch(AppCommand::OpenModal(Modal::Rename { target: RenameTarget::Step { bank, pattern, step }, name: current_name, })); } KeyCode::Char('o') => { ctx.app.audio.config.layout = ctx.app.audio.config.layout.next(); } KeyCode::Char('?') => { ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 })); } KeyCode::Char('e') | KeyCode::Char('E') => { let (bank, pattern, step) = ( ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern, ctx.app.editor_ctx.step, ); let pattern_len = ctx.app.current_edit_pattern().length; let default_steps = pattern_len.min(32); let default_pulses = (default_steps / 2).max(1).min(default_steps); ctx.dispatch(AppCommand::OpenModal(Modal::EuclideanDistribution { bank, pattern, source_step: step, field: EuclideanField::Pulses, pulses: default_pulses.to_string(), steps: default_steps.to_string(), rotation: "0".to_string(), })); } KeyCode::Char('m') => { let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern); ctx.dispatch(AppCommand::StageMute { bank, pattern }); } KeyCode::Char('x') => { let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern); ctx.dispatch(AppCommand::StageSolo { bank, pattern }); } KeyCode::Char('M') => { ctx.dispatch(AppCommand::ClearMutes); ctx.app.send_mute_state(ctx.seq_cmd_tx); } KeyCode::Char('X') => { ctx.dispatch(AppCommand::ClearSolos); ctx.app.send_mute_state(ctx.seq_cmd_tx); } KeyCode::Char('d') => { ctx.dispatch(AppCommand::OpenPreludeEditor); } KeyCode::Char('D') => { ctx.dispatch(AppCommand::EvaluatePrelude); } _ => {} } InputResult::Continue }