This commit is contained in:
2026-01-19 14:42:14 +01:00
parent 9938b356cd
commit 2900f84b7d
17 changed files with 20059 additions and 12 deletions

375
seq/src/app.rs Normal file
View File

@@ -0,0 +1,375 @@
use std::path::PathBuf;
use std::time::Instant;
use tui_textarea::TextArea;
use crate::file;
use crate::link::LinkState;
use crate::model::{Pattern, Project};
use crate::page::Page;
use crate::script::{ScriptEngine, StepContext};
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Focus {
Sequencer,
Editor,
}
#[derive(Clone, PartialEq, Eq)]
pub enum Modal {
None,
ConfirmQuit,
SaveAs(String),
LoadFrom(String),
PatternPicker { cursor: usize },
BankPicker { cursor: usize },
}
pub struct App {
pub tempo: f64,
pub beat: f64,
pub phase: f64,
pub peers: u64,
pub playing: bool,
#[allow(dead_code)]
pub quantum: f64,
pub project: Project,
pub focus: Focus,
pub page: Page,
pub current_step: usize,
pub playback_step: usize,
pub edit_bank: usize,
pub edit_pattern: usize,
pub playback_bank: usize,
pub playback_pattern: usize,
pub queued_bank: Option<usize>,
pub queued_pattern: Option<usize>,
pub event_count: usize,
pub active_voices: usize,
pub peak_voices: usize,
pub cpu_load: f32,
pub schedule_depth: usize,
pub sample_pool_mb: f32,
pub scope: [f32; 64],
pub script_engine: ScriptEngine,
pub file_path: Option<PathBuf>,
pub status_message: Option<String>,
pub editor: TextArea<'static>,
pub flash_until: Option<Instant>,
pub modal: Modal,
pub clipboard: Option<arboard::Clipboard>,
}
impl App {
pub fn new(tempo: f64, quantum: f64) -> Self {
Self {
tempo,
beat: 0.0,
phase: 0.0,
peers: 0,
playing: true,
quantum,
project: Project::default(),
focus: Focus::Sequencer,
page: Page::default(),
current_step: 0,
playback_step: 0,
edit_bank: 0,
edit_pattern: 0,
playback_bank: 0,
playback_pattern: 0,
queued_bank: None,
queued_pattern: None,
event_count: 0,
active_voices: 0,
peak_voices: 0,
cpu_load: 0.0,
schedule_depth: 0,
sample_pool_mb: 0.0,
scope: [0.0; 64],
script_engine: ScriptEngine::new(),
file_path: None,
status_message: None,
editor: TextArea::default(),
flash_until: None,
modal: Modal::None,
clipboard: arboard::Clipboard::new().ok(),
}
}
pub fn update_from_link(&mut self, link: &LinkState) {
let (tempo, beat, phase, peers) = link.query();
self.tempo = tempo;
self.beat = beat;
self.phase = phase;
self.peers = peers;
}
pub fn toggle_playing(&mut self) {
self.playing = !self.playing;
}
pub fn tempo_up(&mut self, link: &LinkState) {
self.tempo = (self.tempo + 1.0).min(300.0);
link.set_tempo(self.tempo);
}
pub fn tempo_down(&mut self, link: &LinkState) {
self.tempo = (self.tempo - 1.0).max(20.0);
link.set_tempo(self.tempo);
}
pub fn toggle_focus(&mut self) {
match self.focus {
Focus::Sequencer => {
self.focus = Focus::Editor;
self.load_step_to_editor();
}
Focus::Editor => {
self.save_editor_to_step();
self.compile_current_step();
self.focus = Focus::Sequencer;
}
}
}
pub fn current_edit_pattern(&self) -> &Pattern {
self.project.pattern_at(self.edit_bank, self.edit_pattern)
}
pub fn next_step(&mut self) {
let len = self.current_edit_pattern().length;
self.current_step = (self.current_step + 1) % len;
self.load_step_to_editor();
}
pub fn prev_step(&mut self) {
let len = self.current_edit_pattern().length;
self.current_step = (self.current_step + len - 1) % len;
self.load_step_to_editor();
}
pub fn step_up(&mut self) {
let len = self.current_edit_pattern().length;
if self.current_step >= 8 {
self.current_step -= 8;
} else {
self.current_step = (self.current_step + len - 8) % len;
}
self.load_step_to_editor();
}
pub fn step_down(&mut self) {
let len = self.current_edit_pattern().length;
self.current_step = (self.current_step + 8) % len;
self.load_step_to_editor();
}
pub fn toggle_step(&mut self) {
let step_idx = self.current_step;
let (bank, pattern) = (self.edit_bank, self.edit_pattern);
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
step.active = !step.active;
}
}
fn load_step_to_editor(&mut self) {
let step_idx = self.current_step;
if let Some(step) = self.current_edit_pattern().step(step_idx) {
let lines: Vec<String> = if step.script.is_empty() {
vec![String::new()]
} else {
step.script.lines().map(String::from).collect()
};
self.editor = TextArea::new(lines);
}
}
pub fn save_editor_to_step(&mut self) {
let text = self.editor.lines().join("\n");
let step_idx = self.current_step;
let (bank, pattern) = (self.edit_bank, self.edit_pattern);
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
step.script = text;
}
}
pub fn compile_current_step(&mut self) {
let step_idx = self.current_step;
let (bank, pattern) = (self.edit_bank, self.edit_pattern);
let script = self
.project
.pattern_at(bank, pattern)
.step(step_idx)
.map(|s| s.script.clone())
.unwrap_or_default();
if script.trim().is_empty() {
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
step.command = None;
}
return;
}
let ctx = StepContext {
step: step_idx,
beat: self.beat,
bank,
pattern,
tempo: self.tempo,
phase: self.phase,
};
match self.script_engine.evaluate(&script, &ctx) {
Ok(cmd) => {
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
step.command = Some(cmd);
}
self.status_message = Some("Script compiled".to_string());
self.flash_until = Some(Instant::now() + std::time::Duration::from_millis(150));
}
Err(e) => {
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
step.command = None;
}
self.status_message = Some(format!("Script error: {e}"));
}
}
}
pub fn compile_all_steps(&mut self) {
let pattern_len = self.current_edit_pattern().length;
let (bank, pattern) = (self.edit_bank, self.edit_pattern);
for step_idx in 0..pattern_len {
let script = self
.project
.pattern_at(bank, pattern)
.step(step_idx)
.map(|s| s.script.clone())
.unwrap_or_default();
if script.trim().is_empty() {
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
step.command = None;
}
continue;
}
let ctx = StepContext {
step: step_idx,
beat: 0.0,
bank,
pattern,
tempo: self.tempo,
phase: 0.0,
};
if let Ok(cmd) = self.script_engine.evaluate(&script, &ctx) {
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
step.command = Some(cmd);
}
}
}
}
pub fn queue_current_for_playback(&mut self) {
self.queued_bank = Some(self.edit_bank);
self.queued_pattern = Some(self.edit_pattern);
self.status_message = Some(format!(
"Queued B{:02} P{:02} (next loop)",
self.edit_bank + 1,
self.edit_pattern + 1
));
}
pub fn select_edit_pattern(&mut self, pattern: usize) {
self.edit_pattern = pattern;
self.current_step = 0;
self.load_step_to_editor();
}
pub fn select_edit_bank(&mut self, bank: usize) {
self.edit_bank = bank;
self.edit_pattern = 0;
self.current_step = 0;
self.load_step_to_editor();
}
pub fn save(&mut self, path: PathBuf) {
self.save_editor_to_step();
match file::save(&self.project, &path) {
Ok(()) => {
self.status_message = Some(format!("Saved: {}", path.display()));
self.file_path = Some(path);
}
Err(e) => {
self.status_message = Some(format!("Save error: {e}"));
}
}
}
pub fn load(&mut self, path: PathBuf) {
match file::load(&path) {
Ok(project) => {
self.project = project;
self.current_step = 0;
self.load_step_to_editor();
self.compile_all_steps();
self.status_message = Some(format!("Loaded: {}", path.display()));
self.file_path = Some(path);
}
Err(e) => {
self.status_message = Some(format!("Load error: {e}"));
}
}
}
pub fn clear_status(&mut self) {
self.status_message = None;
}
pub fn is_flashing(&self) -> bool {
self.flash_until
.map(|t| Instant::now() < t)
.unwrap_or(false)
}
pub fn copy_step(&mut self) {
let step_idx = self.current_step;
let script = self
.current_edit_pattern()
.step(step_idx)
.map(|s| s.script.clone());
if let Some(script) = script {
if let Some(clip) = &mut self.clipboard {
if clip.set_text(&script).is_ok() {
self.status_message = Some("Copied".to_string());
}
}
}
}
pub fn paste_step(&mut self) {
let text = self
.clipboard
.as_mut()
.and_then(|clip| clip.get_text().ok());
if let Some(text) = text {
let step_idx = self.current_step;
let (bank, pattern) = (self.edit_bank, self.edit_pattern);
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
step.script = text;
}
self.load_step_to_editor();
self.compile_current_step();
}
}
}