Refactoring

This commit is contained in:
2026-01-20 02:48:09 +01:00
parent ce0014020f
commit a81716cbd5
18 changed files with 1197 additions and 53253 deletions

416
seq/src/input.rs Normal file
View File

@@ -0,0 +1,416 @@
use crossbeam_channel::Sender;
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use crate::app::App;
use crate::link::LinkState;
use crate::model::PatternSpeed;
use crate::page::Page;
use crate::sequencer::{AudioCommand, SequencerSnapshot};
use crate::state::{AudioFocus, Focus, Modal, PatternField, PatternsViewLevel};
use crate::views::doc_view;
pub enum InputResult {
Continue,
Quit,
}
pub struct InputContext<'a> {
pub app: &'a mut App,
pub link: &'a LinkState,
pub snapshot: &'a SequencerSnapshot,
pub playing: &'a Arc<AtomicBool>,
pub audio_tx: &'a Sender<AudioCommand>,
}
pub fn handle_key(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
ctx.app.clear_status();
if matches!(ctx.app.modal, Modal::None) {
handle_normal_input(ctx, key)
} else {
handle_modal_input(ctx, key)
}
}
fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
match &mut ctx.app.modal {
Modal::ConfirmQuit { selected } => match key.code {
KeyCode::Char('y') | KeyCode::Char('Y') => return InputResult::Quit,
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
ctx.app.modal = Modal::None;
}
KeyCode::Left | KeyCode::Right => {
*selected = !*selected;
}
KeyCode::Enter => {
if *selected {
return InputResult::Quit;
} else {
ctx.app.modal = Modal::None;
}
}
_ => {}
},
Modal::SaveAs(path) => match key.code {
KeyCode::Enter => {
let save_path = PathBuf::from(path.as_str());
ctx.app.modal = Modal::None;
ctx.app.save(save_path);
}
KeyCode::Esc => ctx.app.modal = Modal::None,
KeyCode::Backspace => {
path.pop();
}
KeyCode::Char(c) => path.push(c),
_ => {}
},
Modal::LoadFrom(path) => match key.code {
KeyCode::Enter => {
let load_path = PathBuf::from(path.as_str());
ctx.app.modal = Modal::None;
ctx.app.load(load_path, ctx.link);
}
KeyCode::Esc => ctx.app.modal = Modal::None,
KeyCode::Backspace => {
path.pop();
}
KeyCode::Char(c) => path.push(c),
_ => {}
},
Modal::RenameBank { bank, name } => match key.code {
KeyCode::Enter => {
let bank_idx = *bank;
let new_name = if name.trim().is_empty() {
None
} else {
Some(name.clone())
};
ctx.app.project.banks[bank_idx].name = new_name;
ctx.app.modal = Modal::None;
}
KeyCode::Esc => ctx.app.modal = Modal::None,
KeyCode::Backspace => {
name.pop();
}
KeyCode::Char(c) => name.push(c),
_ => {}
},
Modal::RenamePattern {
bank,
pattern,
name,
} => match key.code {
KeyCode::Enter => {
let (bank_idx, pattern_idx) = (*bank, *pattern);
let new_name = if name.trim().is_empty() {
None
} else {
Some(name.clone())
};
ctx.app.project.banks[bank_idx].patterns[pattern_idx].name = new_name;
ctx.app.modal = Modal::None;
}
KeyCode::Esc => ctx.app.modal = Modal::None,
KeyCode::Backspace => {
name.pop();
}
KeyCode::Char(c) => name.push(c),
_ => {}
},
Modal::SetPattern { field, input } => match key.code {
KeyCode::Enter => {
let field = *field;
let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern);
match field {
PatternField::Length => {
if let Ok(len) = input.parse::<usize>() {
ctx.app
.project
.pattern_at_mut(bank, pattern)
.set_length(len);
let new_len = ctx.app.project.pattern_at(bank, pattern).length;
if ctx.app.editor_ctx.step >= new_len {
ctx.app.editor_ctx.step = new_len - 1;
}
ctx.app.dirty_patterns.insert((bank, pattern));
ctx.app.status_message = Some(format!("Length set to {new_len}"));
} else {
ctx.app.status_message = Some("Invalid length".to_string());
}
}
PatternField::Speed => {
if let Some(speed) = PatternSpeed::from_label(input) {
ctx.app.project.pattern_at_mut(bank, pattern).speed = speed;
ctx.app.dirty_patterns.insert((bank, pattern));
ctx.app.status_message =
Some(format!("Speed set to {}", speed.label()));
} else {
ctx.app.status_message = Some(
"Invalid speed (try 1/8x, 1/4x, 1/2x, 1x, 2x, 4x, 8x)".to_string(),
);
}
}
}
ctx.app.modal = Modal::None;
}
KeyCode::Esc => ctx.app.modal = Modal::None,
KeyCode::Backspace => {
input.pop();
}
KeyCode::Char(c) => input.push(c),
_ => {}
},
Modal::AddSamplePath(path) => match key.code {
KeyCode::Enter => {
let sample_path = PathBuf::from(path.as_str());
if sample_path.is_dir() {
let index = doux::loader::scan_samples_dir(&sample_path);
let count = index.len();
let _ = ctx.audio_tx.send(AudioCommand::LoadSamples(index));
ctx.app.audio.config.sample_count += count;
ctx.app.audio.add_sample_path(sample_path);
ctx.app.status_message = Some(format!("Added {count} samples"));
} else {
ctx.app.status_message = Some("Path is not a directory".to_string());
}
ctx.app.modal = Modal::None;
}
KeyCode::Esc => ctx.app.modal = Modal::None,
KeyCode::Backspace => {
path.pop();
}
KeyCode::Char(c) => path.push(c),
_ => {}
},
Modal::None => unreachable!(),
}
InputResult::Continue
}
fn handle_normal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
// Global navigation with Ctrl+arrows
if ctrl {
match key.code {
KeyCode::Left => {
ctx.app.page.left();
return InputResult::Continue;
}
KeyCode::Right => {
ctx.app.page.right();
return InputResult::Continue;
}
KeyCode::Up => {
ctx.app.page.up();
return InputResult::Continue;
}
KeyCode::Down => {
ctx.app.page.down();
return InputResult::Continue;
}
_ => {}
}
}
match ctx.app.page {
Page::Main => handle_main_page(ctx, key, ctrl),
Page::Patterns => handle_patterns_page(ctx, key),
Page::Audio => handle_audio_page(ctx, key),
Page::Doc => handle_doc_page(ctx, key),
}
}
fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputResult {
match ctx.app.editor_ctx.focus {
Focus::Sequencer => match key.code {
KeyCode::Char('q') => {
ctx.app.modal = Modal::ConfirmQuit { selected: false };
}
KeyCode::Char(' ') => {
ctx.app.toggle_playing();
ctx.playing.store(ctx.app.playing, Ordering::Relaxed);
}
KeyCode::Tab => ctx.app.toggle_focus(ctx.link),
KeyCode::Left => ctx.app.prev_step(),
KeyCode::Right => ctx.app.next_step(),
KeyCode::Up => ctx.app.step_up(),
KeyCode::Down => ctx.app.step_down(),
KeyCode::Enter => ctx.app.toggle_step(),
KeyCode::Char('s') => {
let default = ctx
.app
.file_path
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "project.buboseq".to_string());
ctx.app.modal = Modal::SaveAs(default);
}
KeyCode::Char('l') => {
ctx.app.modal = Modal::LoadFrom(String::new());
}
KeyCode::Char('+') | KeyCode::Char('=') => ctx.app.tempo_up(ctx.link),
KeyCode::Char('-') => ctx.app.tempo_down(ctx.link),
KeyCode::Char('<') | KeyCode::Char(',') => ctx.app.length_decrease(),
KeyCode::Char('>') | KeyCode::Char('.') => ctx.app.length_increase(),
KeyCode::Char('[') => ctx.app.speed_decrease(),
KeyCode::Char(']') => ctx.app.speed_increase(),
KeyCode::Char('L') => ctx.app.open_pattern_modal(PatternField::Length),
KeyCode::Char('S') => ctx.app.open_pattern_modal(PatternField::Speed),
KeyCode::Char('c') if ctrl => ctx.app.copy_step(),
KeyCode::Char('v') if ctrl => ctx.app.paste_step(ctx.link),
_ => {}
},
Focus::Editor => match key.code {
KeyCode::Tab | KeyCode::Esc => ctx.app.toggle_focus(ctx.link),
KeyCode::Char('e') if ctrl => {
ctx.app.save_editor_to_step();
ctx.app.compile_current_step(ctx.link);
}
_ => {
ctx.app.editor_ctx.text.input(Event::Key(key));
}
},
}
InputResult::Continue
}
fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
match key.code {
KeyCode::Left => ctx.app.patterns_cursor = (ctx.app.patterns_cursor + 15) % 16,
KeyCode::Right => ctx.app.patterns_cursor = (ctx.app.patterns_cursor + 1) % 16,
KeyCode::Up => ctx.app.patterns_cursor = (ctx.app.patterns_cursor + 12) % 16,
KeyCode::Down => ctx.app.patterns_cursor = (ctx.app.patterns_cursor + 4) % 16,
KeyCode::Esc | KeyCode::Backspace => match ctx.app.patterns_view_level {
PatternsViewLevel::Banks => ctx.app.page.down(),
PatternsViewLevel::Patterns { .. } => {
ctx.app.patterns_view_level = PatternsViewLevel::Banks;
ctx.app.patterns_cursor = 0;
}
},
KeyCode::Enter => match ctx.app.patterns_view_level {
PatternsViewLevel::Banks => {
let bank = ctx.app.patterns_cursor;
ctx.app.patterns_view_level = PatternsViewLevel::Patterns { bank };
ctx.app.patterns_cursor = 0;
}
PatternsViewLevel::Patterns { bank } => {
let pattern = ctx.app.patterns_cursor;
ctx.app.select_edit_bank(bank);
ctx.app.select_edit_pattern(pattern);
ctx.app.patterns_view_level = PatternsViewLevel::Banks;
ctx.app.patterns_cursor = 0;
ctx.app.page.down();
}
},
KeyCode::Char(' ') => {
if let PatternsViewLevel::Patterns { bank } = ctx.app.patterns_view_level {
let pattern = ctx.app.patterns_cursor;
ctx.app.toggle_pattern_playback(bank, pattern, ctx.snapshot);
}
}
KeyCode::Char('q') => {
ctx.app.modal = Modal::ConfirmQuit { selected: false };
}
KeyCode::Char('r') => match ctx.app.patterns_view_level {
PatternsViewLevel::Banks => {
let bank = ctx.app.patterns_cursor;
let current_name = ctx.app.project.banks[bank].name.clone().unwrap_or_default();
ctx.app.modal = Modal::RenameBank {
bank,
name: current_name,
};
}
PatternsViewLevel::Patterns { bank } => {
let pattern = ctx.app.patterns_cursor;
let current_name = ctx.app.project.banks[bank].patterns[pattern]
.name
.clone()
.unwrap_or_default();
ctx.app.modal = Modal::RenamePattern {
bank,
pattern,
name: current_name,
};
}
},
_ => {}
}
InputResult::Continue
}
fn handle_audio_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
match key.code {
KeyCode::Char('q') => {
ctx.app.modal = Modal::ConfirmQuit { selected: false };
}
KeyCode::Up | KeyCode::Char('k') => ctx.app.audio.prev_focus(),
KeyCode::Down | KeyCode::Char('j') => ctx.app.audio.next_focus(),
KeyCode::Left => match ctx.app.audio.focus {
AudioFocus::OutputDevice => ctx.app.audio.prev_output_device(),
AudioFocus::InputDevice => ctx.app.audio.prev_input_device(),
AudioFocus::Channels => ctx.app.audio.adjust_channels(-1),
AudioFocus::BufferSize => ctx.app.audio.adjust_buffer_size(-64),
AudioFocus::SamplePaths => ctx.app.audio.remove_last_sample_path(),
},
KeyCode::Right => match ctx.app.audio.focus {
AudioFocus::OutputDevice => ctx.app.audio.next_output_device(),
AudioFocus::InputDevice => ctx.app.audio.next_input_device(),
AudioFocus::Channels => ctx.app.audio.adjust_channels(1),
AudioFocus::BufferSize => ctx.app.audio.adjust_buffer_size(64),
AudioFocus::SamplePaths => {}
},
KeyCode::Char('R') => ctx.app.audio.trigger_restart(),
KeyCode::Char('A') => ctx.app.modal = Modal::AddSamplePath(String::new()),
KeyCode::Char('D') => {
ctx.app.audio.refresh_devices();
let out_count = ctx.app.audio.output_devices.len();
let in_count = ctx.app.audio.input_devices.len();
ctx.app.status_message = Some(format!(
"Found {out_count} output, {in_count} input devices"
));
}
KeyCode::Char('h') => {
let _ = ctx.audio_tx.send(AudioCommand::Hush);
}
KeyCode::Char('p') => {
let _ = ctx.audio_tx.send(AudioCommand::Panic);
}
KeyCode::Char('r') => ctx.app.metrics.peak_voices = 0,
KeyCode::Char('t') => {
let _ = ctx
.audio_tx
.send(AudioCommand::Evaluate("sin 440 * 0.3".into()));
}
KeyCode::Char(' ') => {
ctx.app.toggle_playing();
ctx.playing.store(ctx.app.playing, Ordering::Relaxed);
}
_ => {}
}
InputResult::Continue
}
fn handle_doc_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
let topic_count = doc_view::topic_count();
match key.code {
KeyCode::Char('j') | KeyCode::Down => {
ctx.app.doc_topic = (ctx.app.doc_topic + 1) % topic_count;
ctx.app.doc_scroll = 0;
}
KeyCode::Char('k') | KeyCode::Up => {
ctx.app.doc_topic = (ctx.app.doc_topic + topic_count - 1) % topic_count;
ctx.app.doc_scroll = 0;
}
KeyCode::PageDown => ctx.app.doc_scroll = ctx.app.doc_scroll.saturating_add(10),
KeyCode::PageUp => ctx.app.doc_scroll = ctx.app.doc_scroll.saturating_sub(10),
KeyCode::Char('q') => {
ctx.app.modal = Modal::ConfirmQuit { selected: false };
}
_ => {}
}
InputResult::Continue
}