Feat: begin slight refactoring

This commit is contained in:
2026-02-01 12:38:48 +01:00
parent 5b4a6ddd14
commit c356aebfde
39 changed files with 4699 additions and 3168 deletions

View File

@@ -120,14 +120,24 @@ impl App {
midi: crate::settings::MidiSettings {
output_devices: {
let outputs = crate::midi::list_midi_outputs();
self.midi.selected_outputs.iter()
.map(|opt| opt.and_then(|idx| outputs.get(idx).map(|d| d.name.clone())).unwrap_or_default())
self.midi
.selected_outputs
.iter()
.map(|opt| {
opt.and_then(|idx| outputs.get(idx).map(|d| d.name.clone()))
.unwrap_or_default()
})
.collect()
},
input_devices: {
let inputs = crate::midi::list_midi_inputs();
self.midi.selected_inputs.iter()
.map(|opt| opt.and_then(|idx| inputs.get(idx).map(|d| d.name.clone())).unwrap_or_default())
self.midi
.selected_inputs
.iter()
.map(|opt| {
opt.and_then(|idx| inputs.get(idx).map(|d| d.name.clone()))
.unwrap_or_default()
})
.collect()
},
},
@@ -139,6 +149,21 @@ impl App {
(self.editor_ctx.bank, self.editor_ctx.pattern)
}
fn selected_steps(&self) -> Vec<usize> {
match self.editor_ctx.selection_range() {
Some(range) => range.collect(),
None => vec![self.editor_ctx.step],
}
}
fn annotate_copy_name(name: &Option<String>) -> Option<String> {
match name {
Some(n) if !n.ends_with(" (copy)") => Some(format!("{n} (copy)")),
Some(n) => Some(n.clone()),
None => Some("(copy)".to_string()),
}
}
pub fn mark_all_patterns_dirty(&mut self) {
self.project_state.mark_all_dirty();
}
@@ -216,17 +241,8 @@ impl App {
pub fn toggle_steps(&mut self) {
let (bank, pattern) = self.current_bank_pattern();
let indices: Vec<usize> = match self.editor_ctx.selection_range() {
Some(range) => range.collect(),
None => vec![self.editor_ctx.step],
};
for idx in indices {
pattern_editor::toggle_step(
&mut self.project_state.project,
bank,
pattern,
idx,
);
for idx in self.selected_steps() {
pattern_editor::toggle_step(&mut self.project_state.project, bank, pattern, idx);
}
self.project_state.mark_dirty(bank, pattern);
}
@@ -261,6 +277,37 @@ impl App {
self.project_state.mark_dirty(change.bank, change.pattern);
}
fn create_step_context(&self, step_idx: usize, link: &LinkState) -> StepContext {
let (bank, pattern) = self.current_bank_pattern();
let speed = self
.project_state
.project
.pattern_at(bank, pattern)
.speed
.multiplier();
StepContext {
step: step_idx,
beat: link.beat(),
bank,
pattern,
tempo: link.tempo(),
phase: link.phase(),
slot: 0,
runs: 0,
iter: 0,
speed,
fill: false,
nudge_secs: 0.0,
cc_access: None,
#[cfg(feature = "desktop")]
mouse_x: 0.5,
#[cfg(feature = "desktop")]
mouse_y: 0.5,
#[cfg(feature = "desktop")]
mouse_down: 0.0,
}
}
fn load_step_to_editor(&mut self) {
let (bank, pattern) = self.current_bank_pattern();
if let Some(script) = pattern_editor::get_step_script(
@@ -310,37 +357,7 @@ impl App {
link: &LinkState,
audio_tx: &arc_swap::ArcSwap<Sender<crate::engine::AudioCommand>>,
) -> Result<(), String> {
let (bank, pattern) = self.current_bank_pattern();
let step_idx = self.editor_ctx.step;
let speed = self
.project_state
.project
.pattern_at(bank, pattern)
.speed
.multiplier();
let ctx = StepContext {
step: step_idx,
beat: link.beat(),
bank,
pattern,
tempo: link.tempo(),
phase: link.phase(),
slot: 0,
runs: 0,
iter: 0,
speed,
fill: false,
nudge_secs: 0.0,
cc_memory: None,
#[cfg(feature = "desktop")]
mouse_x: 0.5,
#[cfg(feature = "desktop")]
mouse_y: 0.5,
#[cfg(feature = "desktop")]
mouse_down: 0.0,
};
let ctx = self.create_step_context(self.editor_ctx.step, link);
let cmds = self.script_engine.evaluate(script, &ctx)?;
for cmd in cmds {
let _ = audio_tx
@@ -370,33 +387,7 @@ impl App {
return;
}
let speed = self
.project_state
.project
.pattern_at(bank, pattern)
.speed
.multiplier();
let ctx = StepContext {
step: step_idx,
beat: link.beat(),
bank,
pattern,
tempo: link.tempo(),
phase: link.phase(),
slot: 0,
runs: 0,
iter: 0,
speed,
fill: false,
nudge_secs: 0.0,
cc_memory: None,
#[cfg(feature = "desktop")]
mouse_x: 0.5,
#[cfg(feature = "desktop")]
mouse_y: 0.5,
#[cfg(feature = "desktop")]
mouse_down: 0.0,
};
let ctx = self.create_step_context(step_idx, link);
match self.script_engine.evaluate(&script, &ctx) {
Ok(cmds) => {
@@ -454,33 +445,7 @@ impl App {
continue;
}
let speed = self
.project_state
.project
.pattern_at(bank, pattern)
.speed
.multiplier();
let ctx = StepContext {
step: step_idx,
beat: 0.0,
bank,
pattern,
tempo: link.tempo(),
phase: 0.0,
slot: 0,
runs: 0,
iter: 0,
speed,
fill: false,
nudge_secs: 0.0,
cc_memory: None,
#[cfg(feature = "desktop")]
mouse_x: 0.5,
#[cfg(feature = "desktop")]
mouse_y: 0.5,
#[cfg(feature = "desktop")]
mouse_down: 0.0,
};
let ctx = self.create_step_context(step_idx, link);
if let Ok(cmds) = self.script_engine.evaluate(&script, &ctx) {
if let Some(step) = self
@@ -582,7 +547,8 @@ impl App {
self.project_state.project.tempo = link.tempo();
match model::save(&self.project_state.project, &path) {
Ok(final_path) => {
self.ui.set_status(format!("Saved: {}", final_path.display()));
self.ui
.set_status(format!("Saved: {}", final_path.display()));
self.project_state.file_path = Some(final_path);
}
Err(e) => {
@@ -715,11 +681,7 @@ impl App {
pub fn paste_pattern(&mut self, bank: usize, pattern: usize) {
if let Some(src) = &self.copied_pattern {
let mut pat = src.clone();
pat.name = match &src.name {
Some(name) if !name.ends_with(" (copy)") => Some(format!("{name} (copy)")),
Some(name) => Some(name.clone()),
None => Some("(copy)".to_string()),
};
pat.name = Self::annotate_copy_name(&src.name);
self.project_state.project.banks[bank].patterns[pattern] = pat;
self.project_state.mark_dirty(bank, pattern);
if self.editor_ctx.bank == bank && self.editor_ctx.pattern == pattern {
@@ -738,11 +700,7 @@ impl App {
pub fn paste_bank(&mut self, bank: usize) {
if let Some(src) = &self.copied_bank {
let mut b = src.clone();
b.name = match &src.name {
Some(name) if !name.ends_with(" (copy)") => Some(format!("{name} (copy)")),
Some(name) => Some(name.clone()),
None => Some("(copy)".to_string()),
};
b.name = Self::annotate_copy_name(&src.name);
self.project_state.project.banks[bank] = b;
for pattern in 0..self.project_state.project.banks[bank].patterns.len() {
self.project_state.mark_dirty(bank, pattern);
@@ -756,10 +714,7 @@ impl App {
pub fn harden_steps(&mut self) {
let (bank, pattern) = self.current_bank_pattern();
let indices: Vec<usize> = match self.editor_ctx.selection_range() {
Some(range) => range.collect(),
None => vec![self.editor_ctx.step],
};
let indices = self.selected_steps();
let pat = self.project_state.project.pattern_at(bank, pattern);
let resolutions: Vec<(usize, String)> = indices
@@ -796,18 +751,15 @@ impl App {
if count == 1 {
self.ui.flash("Step hardened", 150, FlashKind::Success);
} else {
self.ui.flash(&format!("{count} steps hardened"), 150, FlashKind::Success);
self.ui
.flash(&format!("{count} steps hardened"), 150, FlashKind::Success);
}
}
pub fn copy_steps(&mut self) {
let (bank, pattern) = self.current_bank_pattern();
let pat = self.project_state.project.pattern_at(bank, pattern);
let indices: Vec<usize> = match self.editor_ctx.selection_range() {
Some(range) => range.collect(),
None => vec![self.editor_ctx.step],
};
let indices = self.selected_steps();
let mut steps = Vec::new();
let mut scripts = Vec::new();
@@ -836,7 +788,8 @@ impl App {
let _ = clip.set_text(scripts.join("\n"));
}
self.ui.flash(&format!("Copied {count} steps"), 150, FlashKind::Info);
self.ui
.flash(&format!("Copied {count} steps"), 150, FlashKind::Info);
}
pub fn paste_steps(&mut self, link: &LinkState) {
@@ -855,7 +808,12 @@ impl App {
if target >= pat_len {
break;
}
if let Some(step) = self.project_state.project.pattern_at_mut(bank, pattern).step_mut(target) {
if let Some(step) = self
.project_state
.project
.pattern_at_mut(bank, pattern)
.step_mut(target)
{
let source = if same_pattern { data.source } else { None };
step.active = data.active;
step.source = source;
@@ -885,7 +843,11 @@ impl App {
}
self.editor_ctx.clear_selection();
self.ui.flash(&format!("Pasted {} steps", copied.steps.len()), 150, FlashKind::Success);
self.ui.flash(
&format!("Pasted {} steps", copied.steps.len()),
150,
FlashKind::Success,
);
}
pub fn link_paste_steps(&mut self) {
@@ -897,7 +859,8 @@ impl App {
let (bank, pattern) = self.current_bank_pattern();
if copied.bank != bank || copied.pattern != pattern {
self.ui.set_status("Can only link within same pattern".to_string());
self.ui
.set_status("Can only link within same pattern".to_string());
return;
}
@@ -918,7 +881,12 @@ impl App {
if source_idx == Some(target) {
continue;
}
if let Some(step) = self.project_state.project.pattern_at_mut(bank, pattern).step_mut(target) {
if let Some(step) = self
.project_state
.project
.pattern_at_mut(bank, pattern)
.step_mut(target)
{
step.source = source_idx;
step.script.clear();
step.command = None;
@@ -928,17 +896,18 @@ impl App {
self.project_state.mark_dirty(bank, pattern);
self.load_step_to_editor();
self.editor_ctx.clear_selection();
self.ui.flash(&format!("Linked {} steps", copied.steps.len()), 150, FlashKind::Success);
self.ui.flash(
&format!("Linked {} steps", copied.steps.len()),
150,
FlashKind::Success,
);
}
pub fn duplicate_steps(&mut self, link: &LinkState) {
let (bank, pattern) = self.current_bank_pattern();
let pat = self.project_state.project.pattern_at(bank, pattern);
let pat_len = pat.length;
let indices: Vec<usize> = match self.editor_ctx.selection_range() {
Some(range) => range.collect(),
None => vec![self.editor_ctx.step],
};
let indices = self.selected_steps();
let count = indices.len();
let paste_at = *indices.last().unwrap() + 1;
@@ -958,7 +927,12 @@ impl App {
if target >= pat_len {
break;
}
if let Some(step) = self.project_state.project.pattern_at_mut(bank, pattern).step_mut(target) {
if let Some(step) = self
.project_state
.project
.pattern_at_mut(bank, pattern)
.step_mut(target)
{
step.active = active;
step.source = source;
if source.is_some() {
@@ -984,7 +958,11 @@ impl App {
}
self.editor_ctx.clear_selection();
self.ui.flash(&format!("Duplicated {count} steps"), 150, FlashKind::Success);
self.ui.flash(
&format!("Duplicated {count} steps"),
150,
FlashKind::Success,
);
}
pub fn open_pattern_modal(&mut self, field: PatternField) {
@@ -1139,7 +1117,9 @@ impl App {
step,
name,
} => {
if let Some(s) = self.project_state.project.banks[bank].patterns[pattern].step_mut(step) {
if let Some(s) =
self.project_state.project.banks[bank].patterns[pattern].step_mut(step)
{
s.name = name;
}
self.project_state.mark_dirty(bank, pattern);
@@ -1323,6 +1303,160 @@ impl App {
let pattern = self.patterns_nav.selected_pattern();
self.stage_pattern_toggle(bank, pattern, snapshot);
}
// UI state
AppCommand::ClearMinimap => {
self.ui.minimap_until = None;
}
AppCommand::HideTitle => {
self.ui.show_title = false;
}
AppCommand::ToggleEditorStack => {
self.editor_ctx.show_stack = !self.editor_ctx.show_stack;
}
AppCommand::SetColorScheme(scheme) => {
self.ui.color_scheme = scheme;
crate::theme::set(scheme.to_theme());
}
AppCommand::ToggleRuntimeHighlight => {
self.ui.runtime_highlight = !self.ui.runtime_highlight;
}
AppCommand::ToggleCompletion => {
self.ui.show_completion = !self.ui.show_completion;
self.editor_ctx
.editor
.set_completion_enabled(self.ui.show_completion);
}
AppCommand::AdjustFlashBrightness(delta) => {
self.ui.flash_brightness = (self.ui.flash_brightness + delta).clamp(0.0, 1.0);
}
// Live keys
AppCommand::ToggleLiveKeysFill => {
self.live_keys.flip_fill();
}
// Panel
AppCommand::ClosePanel => {
self.panel.visible = false;
self.panel.focus = crate::state::PanelFocus::Main;
}
// Selection
AppCommand::SetSelectionAnchor(step) => {
self.editor_ctx.selection_anchor = Some(step);
}
AppCommand::ClearSelectionAnchor => {
self.editor_ctx.selection_anchor = None;
}
// Audio settings (engine page)
AppCommand::AudioNextSection => {
self.audio.next_section();
}
AppCommand::AudioPrevSection => {
self.audio.prev_section();
}
AppCommand::AudioOutputListUp => {
self.audio.output_list.move_up();
}
AppCommand::AudioOutputListDown(count) => {
self.audio.output_list.move_down(count);
}
AppCommand::AudioOutputPageUp => {
self.audio.output_list.page_up();
}
AppCommand::AudioOutputPageDown(count) => {
self.audio.output_list.page_down(count);
}
AppCommand::AudioInputListUp => {
self.audio.input_list.move_up();
}
AppCommand::AudioInputListDown(count) => {
self.audio.input_list.move_down(count);
}
AppCommand::AudioInputPageDown(count) => {
self.audio.input_list.page_down(count);
}
AppCommand::AudioSettingNext => {
self.audio.setting_kind = self.audio.setting_kind.next();
}
AppCommand::AudioSettingPrev => {
self.audio.setting_kind = self.audio.setting_kind.prev();
}
AppCommand::SetOutputDevice(name) => {
self.audio.config.output_device = Some(name);
}
AppCommand::SetInputDevice(name) => {
self.audio.config.input_device = Some(name);
}
AppCommand::SetDeviceKind(kind) => {
self.audio.device_kind = kind;
}
AppCommand::AdjustAudioSetting { setting, delta } => {
use crate::state::SettingKind;
match setting {
SettingKind::Channels => self.audio.adjust_channels(delta as i16),
SettingKind::BufferSize => self.audio.adjust_buffer_size(delta),
SettingKind::Polyphony => self.audio.adjust_max_voices(delta),
SettingKind::Nudge => {
self.metrics.nudge_ms =
(self.metrics.nudge_ms + delta as f64).clamp(-50.0, 50.0);
}
SettingKind::Lookahead => self.audio.adjust_lookahead(delta),
}
}
AppCommand::AudioTriggerRestart => {
self.audio.trigger_restart();
}
AppCommand::RemoveLastSamplePath => {
self.audio.remove_last_sample_path();
}
AppCommand::AudioRefreshDevices => {
self.audio.refresh_devices();
}
// Options page
AppCommand::OptionsNextFocus => {
self.options.next_focus();
}
AppCommand::OptionsPrevFocus => {
self.options.prev_focus();
}
AppCommand::ToggleRefreshRate => {
self.audio.toggle_refresh_rate();
}
AppCommand::ToggleScope => {
self.audio.config.show_scope = !self.audio.config.show_scope;
}
AppCommand::ToggleSpectrum => {
self.audio.config.show_spectrum = !self.audio.config.show_spectrum;
}
// Metrics
AppCommand::ResetPeakVoices => {
self.metrics.peak_voices = 0;
}
// MIDI connections
AppCommand::ConnectMidiOutput { slot, port } => {
if let Err(e) = self.midi.connect_output(slot, port) {
self.ui
.flash(&format!("MIDI error: {e}"), 300, FlashKind::Error);
}
}
AppCommand::DisconnectMidiOutput(slot) => {
self.midi.disconnect_output(slot);
}
AppCommand::ConnectMidiInput { slot, port } => {
if let Err(e) = self.midi.connect_input(slot, port) {
self.ui
.flash(&format!("MIDI error: {e}"), 300, FlashKind::Error);
}
}
AppCommand::DisconnectMidiInput(slot) => {
self.midi.disconnect_input(slot);
}
}
}

View File

@@ -8,12 +8,10 @@ use eframe::NativeOptions;
use egui_ratatui::RataguiBackend;
use ratatui::Terminal;
use soft_ratatui::embedded_graphics_unicodefonts::{
mono_6x13_atlas, mono_6x13_bold_atlas, mono_6x13_italic_atlas,
mono_7x13_atlas, mono_7x13_bold_atlas, mono_7x13_italic_atlas,
mono_8x13_atlas, mono_8x13_bold_atlas, mono_8x13_italic_atlas,
mono_9x15_atlas, mono_9x15_bold_atlas,
mono_10x20_atlas, mono_6x13_atlas, mono_6x13_bold_atlas, mono_6x13_italic_atlas,
mono_7x13_atlas, mono_7x13_bold_atlas, mono_7x13_italic_atlas, mono_8x13_atlas,
mono_8x13_bold_atlas, mono_8x13_italic_atlas, mono_9x15_atlas, mono_9x15_bold_atlas,
mono_9x18_atlas, mono_9x18_bold_atlas,
mono_10x20_atlas,
};
use soft_ratatui::{EmbeddedGraphics, SoftBackend};
@@ -22,12 +20,12 @@ use cagire::engine::{
build_stream, spawn_sequencer, AnalysisHandle, AudioStreamConfig, LinkState, MidiCommand,
ScopeBuffer, SequencerConfig, SequencerHandle, SpectrumBuffer,
};
use crossbeam_channel::Receiver;
use cagire::input::{handle_key, InputContext, InputResult};
use cagire::input_egui::convert_egui_events;
use cagire::settings::Settings;
use cagire::state::audio::RefreshRate;
use cagire::views;
use crossbeam_channel::Receiver;
#[derive(Parser)]
#[command(name = "cagire-desktop", about = "Cagire desktop application")]
@@ -214,7 +212,9 @@ impl CagireDesktop {
audio_sample_pos: Arc::clone(&audio_sample_pos),
sample_rate: Arc::clone(&sample_rate_shared),
lookahead_ms: Arc::clone(&lookahead_ms),
cc_memory: Some(Arc::clone(&app.midi.cc_memory)),
cc_access: Some(
Arc::new(app.midi.cc_memory.clone()) as Arc<dyn cagire::model::CcAccess>
),
mouse_x: Arc::clone(&mouse_x),
mouse_y: Arc::clone(&mouse_y),
mouse_down: Arc::clone(&mouse_down),
@@ -349,7 +349,11 @@ impl CagireDesktop {
self.app.metrics.active_voices =
self.metrics.active_voices.load(Ordering::Relaxed) as usize;
self.app.metrics.peak_voices = self.app.metrics.peak_voices.max(self.app.metrics.active_voices);
self.app.metrics.peak_voices = self
.app
.metrics
.peak_voices
.max(self.app.metrics.active_voices);
self.app.metrics.cpu_load = self.metrics.load.get_load();
self.app.metrics.schedule_depth =
self.metrics.schedule_depth.load(Ordering::Relaxed) as usize;
@@ -399,7 +403,11 @@ impl eframe::App for CagireDesktop {
self.mouse_x.store(nx.to_bits(), Ordering::Relaxed);
self.mouse_y.store(ny.to_bits(), Ordering::Relaxed);
}
let down = if i.pointer.primary_down() { 1.0_f32 } else { 0.0_f32 };
let down = if i.pointer.primary_down() {
1.0_f32
} else {
0.0_f32
};
self.mouse_down.store(down.to_bits(), Ordering::Relaxed);
});
@@ -427,22 +435,48 @@ impl eframe::App for CagireDesktop {
while let Ok(midi_cmd) = self.midi_rx.try_recv() {
match midi_cmd {
MidiCommand::NoteOn { device, channel, note, velocity } => {
MidiCommand::NoteOn {
device,
channel,
note,
velocity,
} => {
self.app.midi.send_note_on(device, channel, note, velocity);
}
MidiCommand::NoteOff { device, channel, note } => {
MidiCommand::NoteOff {
device,
channel,
note,
} => {
self.app.midi.send_note_off(device, channel, note);
}
MidiCommand::CC { device, channel, cc, value } => {
MidiCommand::CC {
device,
channel,
cc,
value,
} => {
self.app.midi.send_cc(device, channel, cc, value);
}
MidiCommand::PitchBend { device, channel, value } => {
MidiCommand::PitchBend {
device,
channel,
value,
} => {
self.app.midi.send_pitch_bend(device, channel, value);
}
MidiCommand::Pressure { device, channel, value } => {
MidiCommand::Pressure {
device,
channel,
value,
} => {
self.app.midi.send_pressure(device, channel, value);
}
MidiCommand::ProgramChange { device, channel, program } => {
MidiCommand::ProgramChange {
device,
channel,
program,
} => {
self.app.midi.send_program_change(device, channel, program);
}
MidiCommand::Clock { device } => self.app.midi.send_realtime(device, 0xF8),

View File

@@ -1,7 +1,7 @@
use std::path::PathBuf;
use crate::model::{LaunchQuantization, PatternSpeed, SyncMode};
use crate::state::{FlashKind, Modal, PatternField};
use crate::state::{ColorScheme, DeviceKind, FlashKind, Modal, PatternField, SettingKind};
#[allow(dead_code)]
pub enum AppCommand {
@@ -169,4 +169,68 @@ pub enum AppCommand {
PatternsEnter,
PatternsBack,
PatternsTogglePlay,
// UI state
ClearMinimap,
HideTitle,
ToggleEditorStack,
SetColorScheme(ColorScheme),
ToggleRuntimeHighlight,
ToggleCompletion,
AdjustFlashBrightness(f32),
// Live keys
ToggleLiveKeysFill,
// Panel
ClosePanel,
// Selection
SetSelectionAnchor(usize),
ClearSelectionAnchor,
// Audio settings (engine page)
AudioNextSection,
AudioPrevSection,
AudioOutputListUp,
AudioOutputListDown(usize),
AudioOutputPageUp,
AudioOutputPageDown(usize),
AudioInputListUp,
AudioInputListDown(usize),
AudioInputPageDown(usize),
AudioSettingNext,
AudioSettingPrev,
SetOutputDevice(String),
SetInputDevice(String),
SetDeviceKind(DeviceKind),
AdjustAudioSetting {
setting: SettingKind,
delta: i32,
},
AudioTriggerRestart,
RemoveLastSamplePath,
AudioRefreshDevices,
// Options page
OptionsNextFocus,
OptionsPrevFocus,
ToggleRefreshRate,
ToggleScope,
ToggleSpectrum,
// Metrics
ResetPeakVoices,
// MIDI connections
ConnectMidiOutput {
slot: usize,
port: usize,
},
DisconnectMidiOutput(usize),
ConnectMidiInput {
slot: usize,
port: usize,
},
DisconnectMidiInput(usize),
}

View File

@@ -10,7 +10,9 @@ use std::time::Duration;
use thread_priority::{set_current_thread_priority, ThreadPriority};
use super::LinkState;
use crate::model::{CcMemory, Dictionary, ExecutionTrace, Rng, ScriptEngine, StepContext, Value, Variables};
use crate::model::{
CcAccess, Dictionary, ExecutionTrace, Rng, ScriptEngine, StepContext, Value, Variables,
};
use crate::model::{LaunchQuantization, SyncMode, MAX_BANKS, MAX_PATTERNS};
use crate::state::LiveKeyState;
@@ -55,16 +57,50 @@ pub enum AudioCommand {
#[derive(Clone, Debug)]
pub enum MidiCommand {
NoteOn { device: u8, channel: u8, note: u8, velocity: u8 },
NoteOff { device: u8, channel: u8, note: u8 },
CC { device: u8, channel: u8, cc: u8, value: u8 },
PitchBend { device: u8, channel: u8, value: u16 },
Pressure { device: u8, channel: u8, value: u8 },
ProgramChange { device: u8, channel: u8, program: u8 },
Clock { device: u8 },
Start { device: u8 },
Stop { device: u8 },
Continue { device: u8 },
NoteOn {
device: u8,
channel: u8,
note: u8,
velocity: u8,
},
NoteOff {
device: u8,
channel: u8,
note: u8,
},
CC {
device: u8,
channel: u8,
cc: u8,
value: u8,
},
PitchBend {
device: u8,
channel: u8,
value: u16,
},
Pressure {
device: u8,
channel: u8,
value: u8,
},
ProgramChange {
device: u8,
channel: u8,
program: u8,
},
Clock {
device: u8,
},
Start {
device: u8,
},
Stop {
device: u8,
},
Continue {
device: u8,
},
}
pub enum SeqCommand {
@@ -233,7 +269,7 @@ pub struct SequencerConfig {
pub audio_sample_pos: Arc<AtomicU64>,
pub sample_rate: Arc<std::sync::atomic::AtomicU32>,
pub lookahead_ms: Arc<std::sync::atomic::AtomicU32>,
pub cc_memory: Option<CcMemory>,
pub cc_access: Option<Arc<dyn CcAccess>>,
#[cfg(feature = "desktop")]
pub mouse_x: Arc<AtomicU32>,
#[cfg(feature = "desktop")]
@@ -253,7 +289,11 @@ pub fn spawn_sequencer(
live_keys: Arc<LiveKeyState>,
nudge_us: Arc<AtomicI64>,
config: SequencerConfig,
) -> (SequencerHandle, Receiver<AudioCommand>, Receiver<MidiCommand>) {
) -> (
SequencerHandle,
Receiver<AudioCommand>,
Receiver<MidiCommand>,
) {
let (cmd_tx, cmd_rx) = bounded::<SeqCommand>(64);
let (audio_tx, audio_rx) = bounded::<AudioCommand>(256);
let (midi_tx, midi_rx) = bounded::<MidiCommand>(256);
@@ -291,7 +331,7 @@ pub fn spawn_sequencer(
config.audio_sample_pos,
config.sample_rate,
config.lookahead_ms,
config.cc_memory,
config.cc_access,
#[cfg(feature = "desktop")]
mouse_x,
#[cfg(feature = "desktop")]
@@ -510,12 +550,17 @@ pub(crate) struct SequencerState {
speed_overrides: HashMap<(usize, usize), f64>,
key_cache: KeyCache,
buf_audio_commands: Vec<TimestampedCommand>,
cc_memory: Option<CcMemory>,
cc_access: Option<Arc<dyn CcAccess>>,
active_notes: HashMap<(u8, u8, u8), ActiveNote>,
}
impl SequencerState {
pub fn new(variables: Variables, dict: Dictionary, rng: Rng, cc_memory: Option<CcMemory>) -> Self {
pub fn new(
variables: Variables,
dict: Dictionary,
rng: Rng,
cc_access: Option<Arc<dyn CcAccess>>,
) -> Self {
let script_engine = ScriptEngine::new(Arc::clone(&variables), dict, rng);
Self {
audio_state: AudioState::new(),
@@ -529,7 +574,7 @@ impl SequencerState {
speed_overrides: HashMap::new(),
key_cache: KeyCache::new(),
buf_audio_commands: Vec::new(),
cc_memory,
cc_access,
active_notes: HashMap::new(),
}
}
@@ -781,7 +826,7 @@ impl SequencerState {
speed: speed_mult,
fill,
nudge_secs,
cc_memory: self.cc_memory.clone(),
cc_access: self.cc_access.clone(),
#[cfg(feature = "desktop")]
mouse_x,
#[cfg(feature = "desktop")]
@@ -943,7 +988,7 @@ fn sequencer_loop(
audio_sample_pos: Arc<AtomicU64>,
sample_rate: Arc<std::sync::atomic::AtomicU32>,
lookahead_ms: Arc<std::sync::atomic::AtomicU32>,
cc_memory: Option<CcMemory>,
cc_access: Option<Arc<dyn CcAccess>>,
#[cfg(feature = "desktop")] mouse_x: Arc<AtomicU32>,
#[cfg(feature = "desktop")] mouse_y: Arc<AtomicU32>,
#[cfg(feature = "desktop")] mouse_down: Arc<AtomicU32>,
@@ -952,7 +997,7 @@ fn sequencer_loop(
let _ = set_current_thread_priority(ThreadPriority::Max);
let mut seq_state = SequencerState::new(variables, dict, rng, cc_memory);
let mut seq_state = SequencerState::new(variables, dict, rng, cc_access);
loop {
let mut commands = Vec::new();
@@ -1002,8 +1047,15 @@ fn sequencer_loop(
if let Some((midi_cmd, dur)) = parse_midi_command(&tsc.cmd) {
match midi_tx.load().try_send(midi_cmd.clone()) {
Ok(()) => {
if let (MidiCommand::NoteOn { device, channel, note, .. }, Some(dur_secs)) =
(&midi_cmd, dur)
if let (
MidiCommand::NoteOn {
device,
channel,
note,
..
},
Some(dur_secs),
) = (&midi_cmd, dur)
{
let dur_us = (dur_secs * 1_000_000.0) as i64;
seq_state.active_notes.insert(
@@ -1037,28 +1089,41 @@ fn sequencer_loop(
if output.flush_midi_notes {
for ((device, channel, note), _) in seq_state.active_notes.drain() {
let _ = midi_tx.load().try_send(MidiCommand::NoteOff { device, channel, note });
let _ = midi_tx.load().try_send(MidiCommand::NoteOff {
device,
channel,
note,
});
}
// Send MIDI panic (CC 123 = All Notes Off) on all 16 channels for all devices
for dev in 0..4u8 {
for chan in 0..16u8 {
let _ = midi_tx
.load()
.try_send(MidiCommand::CC { device: dev, channel: chan, cc: 123, value: 0 });
let _ = midi_tx.load().try_send(MidiCommand::CC {
device: dev,
channel: chan,
cc: 123,
value: 0,
});
}
}
} else {
seq_state.active_notes.retain(|&(device, channel, note), active| {
let should_release = current_time_us >= active.off_time_us;
let timed_out = (current_time_us - active.start_time_us) > MAX_NOTE_DURATION_US;
seq_state
.active_notes
.retain(|&(device, channel, note), active| {
let should_release = current_time_us >= active.off_time_us;
let timed_out = (current_time_us - active.start_time_us) > MAX_NOTE_DURATION_US;
if should_release || timed_out {
let _ = midi_tx.load().try_send(MidiCommand::NoteOff { device, channel, note });
false
} else {
true
}
});
if should_release || timed_out {
let _ = midi_tx.load().try_send(MidiCommand::NoteOff {
device,
channel,
note,
});
false
} else {
true
}
});
}
if let Some(t) = output.new_tempo {
@@ -1081,7 +1146,8 @@ fn parse_midi_command(cmd: &str) -> Option<(MidiCommand, Option<f64>)> {
}
let find_param = |key: &str| -> Option<&str> {
parts.iter()
parts
.iter()
.position(|&s| s == key)
.and_then(|i| parts.get(i + 1).copied())
};
@@ -1124,19 +1190,40 @@ fn parse_midi_command(cmd: &str) -> Option<(MidiCommand, Option<f64>)> {
// /midi/bend/<value>/chan/<chan>/dev/<dev>
let value: u16 = parts.get(2)?.parse().ok()?;
let chan: u8 = find_param("chan")?.parse().ok()?;
Some((MidiCommand::PitchBend { device, channel: chan, value }, None))
Some((
MidiCommand::PitchBend {
device,
channel: chan,
value,
},
None,
))
}
"pressure" => {
// /midi/pressure/<value>/chan/<chan>/dev/<dev>
let value: u8 = parts.get(2)?.parse().ok()?;
let chan: u8 = find_param("chan")?.parse().ok()?;
Some((MidiCommand::Pressure { device, channel: chan, value }, None))
Some((
MidiCommand::Pressure {
device,
channel: chan,
value,
},
None,
))
}
"program" => {
// /midi/program/<value>/chan/<chan>/dev/<dev>
let program: u8 = parts.get(2)?.parse().ok()?;
let chan: u8 = find_param("chan")?.parse().ok()?;
Some((MidiCommand::ProgramChange { device, channel: chan, program }, None))
Some((
MidiCommand::ProgramChange {
device,
channel: chan,
program,
},
None,
))
}
"clock" => Some((MidiCommand::Clock { device }, None)),
"start" => Some((MidiCommand::Start { device }, None)),

View File

@@ -52,11 +52,11 @@ pub fn handle_key(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
KeyCode::Left | KeyCode::Right | KeyCode::Up | KeyCode::Down
);
if ctx.app.ui.minimap_until.is_some() && !(ctrl && is_arrow) {
ctx.app.ui.minimap_until = None;
ctx.dispatch(AppCommand::ClearMinimap);
}
if ctx.app.ui.show_title {
ctx.app.ui.show_title = false;
ctx.dispatch(AppCommand::HideTitle);
return InputResult::Continue;
}
@@ -73,7 +73,7 @@ fn handle_live_keys(ctx: &mut InputContext, key: &KeyEvent) -> bool {
match (key.code, key.kind) {
_ if !matches!(ctx.app.ui.modal, Modal::None) => false,
(KeyCode::Char('f'), KeyEventKind::Press) => {
ctx.app.live_keys.flip_fill();
ctx.dispatch(AppCommand::ToggleLiveKeysFill);
true
}
_ => false,
@@ -506,13 +506,23 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
editor.search_prev();
}
KeyCode::Char('s') if ctrl => {
ctx.app.editor_ctx.show_stack = !ctx.app.editor_ctx.show_stack;
ctx.dispatch(AppCommand::ToggleEditorStack);
}
KeyCode::Char('r') if ctrl => {
let script = ctx.app.editor_ctx.editor.lines().join("\n");
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),
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('a') if ctrl => {
@@ -745,8 +755,7 @@ fn handle_panel_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
KeyCode::Left => state.collapse_at_cursor(),
KeyCode::Char('/') => state.activate_search(),
KeyCode::Esc | KeyCode::Tab => {
ctx.app.panel.visible = false;
ctx.app.panel.focus = PanelFocus::Main;
ctx.dispatch(AppCommand::ClosePanel);
}
_ => {}
}
@@ -783,25 +792,25 @@ fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputR
}
KeyCode::Left if shift && !ctrl => {
if ctx.app.editor_ctx.selection_anchor.is_none() {
ctx.app.editor_ctx.selection_anchor = Some(ctx.app.editor_ctx.step);
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.app.editor_ctx.selection_anchor = Some(ctx.app.editor_ctx.step);
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.app.editor_ctx.selection_anchor = Some(ctx.app.editor_ctx.step);
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.app.editor_ctx.selection_anchor = Some(ctx.app.editor_ctx.step);
ctx.dispatch(AppCommand::SetSelectionAnchor(ctx.app.editor_ctx.step));
}
ctx.dispatch(AppCommand::StepDown);
}
@@ -910,9 +919,19 @@ fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputR
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),
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,
),
}
}
}
@@ -923,7 +942,9 @@ fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputR
ctx.app.editor_ctx.pattern,
ctx.app.editor_ctx.step,
);
let current_name = ctx.app.current_edit_pattern()
let current_name = ctx
.app
.current_edit_pattern()
.step(step)
.and_then(|s| s.name.clone())
.unwrap_or_default();
@@ -1063,15 +1084,15 @@ fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
selected: false,
}));
}
KeyCode::Tab => ctx.app.audio.next_section(),
KeyCode::BackTab => ctx.app.audio.prev_section(),
KeyCode::Tab => ctx.dispatch(AppCommand::AudioNextSection),
KeyCode::BackTab => ctx.dispatch(AppCommand::AudioPrevSection),
KeyCode::Up => match ctx.app.audio.section {
EngineSection::Devices => match ctx.app.audio.device_kind {
DeviceKind::Output => ctx.app.audio.output_list.move_up(),
DeviceKind::Input => ctx.app.audio.input_list.move_up(),
DeviceKind::Output => ctx.dispatch(AppCommand::AudioOutputListUp),
DeviceKind::Input => ctx.dispatch(AppCommand::AudioInputListUp),
},
EngineSection::Settings => {
ctx.app.audio.setting_kind = ctx.app.audio.setting_kind.prev();
ctx.dispatch(AppCommand::AudioSettingPrev);
}
EngineSection::Samples => {}
},
@@ -1079,22 +1100,22 @@ fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
EngineSection::Devices => match ctx.app.audio.device_kind {
DeviceKind::Output => {
let count = ctx.app.audio.output_devices.len();
ctx.app.audio.output_list.move_down(count);
ctx.dispatch(AppCommand::AudioOutputListDown(count));
}
DeviceKind::Input => {
let count = ctx.app.audio.input_devices.len();
ctx.app.audio.input_list.move_down(count);
ctx.dispatch(AppCommand::AudioInputListDown(count));
}
},
EngineSection::Settings => {
ctx.app.audio.setting_kind = ctx.app.audio.setting_kind.next();
ctx.dispatch(AppCommand::AudioSettingNext);
}
EngineSection::Samples => {}
},
KeyCode::PageUp => {
if ctx.app.audio.section == EngineSection::Devices {
match ctx.app.audio.device_kind {
DeviceKind::Output => ctx.app.audio.output_list.page_up(),
DeviceKind::Output => ctx.dispatch(AppCommand::AudioOutputPageUp),
DeviceKind::Input => ctx.app.audio.input_list.page_up(),
}
}
@@ -1104,11 +1125,11 @@ fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
match ctx.app.audio.device_kind {
DeviceKind::Output => {
let count = ctx.app.audio.output_devices.len();
ctx.app.audio.output_list.page_down(count);
ctx.dispatch(AppCommand::AudioOutputPageDown(count));
}
DeviceKind::Input => {
let count = ctx.app.audio.input_devices.len();
ctx.app.audio.input_list.page_down(count);
ctx.dispatch(AppCommand::AudioInputPageDown(count));
}
}
}
@@ -1119,16 +1140,16 @@ fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
DeviceKind::Output => {
let cursor = ctx.app.audio.output_list.cursor;
if cursor < ctx.app.audio.output_devices.len() {
ctx.app.audio.config.output_device =
Some(ctx.app.audio.output_devices[cursor].name.clone());
let name = ctx.app.audio.output_devices[cursor].name.clone();
ctx.dispatch(AppCommand::SetOutputDevice(name));
ctx.app.save_settings(ctx.link);
}
}
DeviceKind::Input => {
let cursor = ctx.app.audio.input_list.cursor;
if cursor < ctx.app.audio.input_devices.len() {
ctx.app.audio.config.input_device =
Some(ctx.app.audio.input_devices[cursor].name.clone());
let name = ctx.app.audio.input_devices[cursor].name.clone();
ctx.dispatch(AppCommand::SetInputDevice(name));
ctx.app.save_settings(ctx.link);
}
}
@@ -1137,20 +1158,32 @@ fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
}
KeyCode::Left => match ctx.app.audio.section {
EngineSection::Devices => {
ctx.app.audio.device_kind = DeviceKind::Output;
ctx.dispatch(AppCommand::SetDeviceKind(DeviceKind::Output));
}
EngineSection::Settings => {
match ctx.app.audio.setting_kind {
SettingKind::Channels => ctx.app.audio.adjust_channels(-1),
SettingKind::BufferSize => ctx.app.audio.adjust_buffer_size(-64),
SettingKind::Polyphony => ctx.app.audio.adjust_max_voices(-1),
SettingKind::Channels => ctx.dispatch(AppCommand::AdjustAudioSetting {
setting: SettingKind::Channels,
delta: -1,
}),
SettingKind::BufferSize => ctx.dispatch(AppCommand::AdjustAudioSetting {
setting: SettingKind::BufferSize,
delta: -64,
}),
SettingKind::Polyphony => ctx.dispatch(AppCommand::AdjustAudioSetting {
setting: SettingKind::Polyphony,
delta: -1,
}),
SettingKind::Nudge => {
let prev = ctx.nudge_us.load(Ordering::Relaxed);
ctx.nudge_us
.store((prev - 1000).max(-100_000), Ordering::Relaxed);
}
SettingKind::Lookahead => {
ctx.app.audio.adjust_lookahead(-1);
ctx.dispatch(AppCommand::AdjustAudioSetting {
setting: SettingKind::Lookahead,
delta: -1,
});
ctx.lookahead_ms
.store(ctx.app.audio.config.lookahead_ms, Ordering::Relaxed);
}
@@ -1161,20 +1194,32 @@ fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
},
KeyCode::Right => match ctx.app.audio.section {
EngineSection::Devices => {
ctx.app.audio.device_kind = DeviceKind::Input;
ctx.dispatch(AppCommand::SetDeviceKind(DeviceKind::Input));
}
EngineSection::Settings => {
match ctx.app.audio.setting_kind {
SettingKind::Channels => ctx.app.audio.adjust_channels(1),
SettingKind::BufferSize => ctx.app.audio.adjust_buffer_size(64),
SettingKind::Polyphony => ctx.app.audio.adjust_max_voices(1),
SettingKind::Channels => ctx.dispatch(AppCommand::AdjustAudioSetting {
setting: SettingKind::Channels,
delta: 1,
}),
SettingKind::BufferSize => ctx.dispatch(AppCommand::AdjustAudioSetting {
setting: SettingKind::BufferSize,
delta: 64,
}),
SettingKind::Polyphony => ctx.dispatch(AppCommand::AdjustAudioSetting {
setting: SettingKind::Polyphony,
delta: 1,
}),
SettingKind::Nudge => {
let prev = ctx.nudge_us.load(Ordering::Relaxed);
ctx.nudge_us
.store((prev + 1000).min(100_000), Ordering::Relaxed);
}
SettingKind::Lookahead => {
ctx.app.audio.adjust_lookahead(1);
ctx.dispatch(AppCommand::AdjustAudioSetting {
setting: SettingKind::Lookahead,
delta: 1,
});
ctx.lookahead_ms
.store(ctx.app.audio.config.lookahead_ms, Ordering::Relaxed);
}
@@ -1183,7 +1228,7 @@ fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
}
EngineSection::Samples => {}
},
KeyCode::Char('R') => ctx.app.audio.trigger_restart(),
KeyCode::Char('R') => ctx.dispatch(AppCommand::AudioTriggerRestart),
KeyCode::Char('A') => {
use crate::state::file_browser::FileBrowserState;
let state = FileBrowserState::new_load(String::new());
@@ -1191,9 +1236,9 @@ fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
}
KeyCode::Char('D') => {
if ctx.app.audio.section == EngineSection::Samples {
ctx.app.audio.remove_last_sample_path();
ctx.dispatch(AppCommand::RemoveLastSamplePath);
} else {
ctx.app.audio.refresh_devices();
ctx.dispatch(AppCommand::AudioRefreshDevices);
let out_count = ctx.app.audio.output_devices.len();
let in_count = ctx.app.audio.input_devices.len();
ctx.dispatch(AppCommand::SetStatus(format!(
@@ -1209,7 +1254,7 @@ fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
let _ = ctx.audio_tx.load().send(AudioCommand::Panic);
let _ = ctx.seq_cmd_tx.send(SeqCommand::StopAll);
}
KeyCode::Char('r') => ctx.app.metrics.peak_voices = 0,
KeyCode::Char('r') => ctx.dispatch(AppCommand::ResetPeakVoices),
KeyCode::Char('t') => {
let _ = ctx.audio_tx.load().send(AudioCommand::Evaluate {
cmd: "/sound/sine/dur/0.5/decay/0.2".into(),
@@ -1231,8 +1276,8 @@ fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
selected: false,
}));
}
KeyCode::Down | KeyCode::Tab => ctx.app.options.next_focus(),
KeyCode::Up | KeyCode::BackTab => ctx.app.options.prev_focus(),
KeyCode::Down | KeyCode::Tab => ctx.dispatch(AppCommand::OptionsNextFocus),
KeyCode::Up | KeyCode::BackTab => ctx.dispatch(AppCommand::OptionsPrevFocus),
KeyCode::Left | KeyCode::Right => {
match ctx.app.options.focus {
OptionsFocus::ColorScheme => {
@@ -1241,26 +1286,24 @@ fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
} else {
ctx.app.ui.color_scheme.next()
};
ctx.app.ui.color_scheme = new_scheme;
crate::theme::set(new_scheme.to_theme());
ctx.dispatch(AppCommand::SetColorScheme(new_scheme));
}
OptionsFocus::RefreshRate => ctx.app.audio.toggle_refresh_rate(),
OptionsFocus::RefreshRate => ctx.dispatch(AppCommand::ToggleRefreshRate),
OptionsFocus::RuntimeHighlight => {
ctx.app.ui.runtime_highlight = !ctx.app.ui.runtime_highlight
ctx.dispatch(AppCommand::ToggleRuntimeHighlight);
}
OptionsFocus::ShowScope => {
ctx.app.audio.config.show_scope = !ctx.app.audio.config.show_scope
ctx.dispatch(AppCommand::ToggleScope);
}
OptionsFocus::ShowSpectrum => {
ctx.app.audio.config.show_spectrum = !ctx.app.audio.config.show_spectrum
ctx.dispatch(AppCommand::ToggleSpectrum);
}
OptionsFocus::ShowCompletion => {
ctx.app.ui.show_completion = !ctx.app.ui.show_completion
ctx.dispatch(AppCommand::ToggleCompletion);
}
OptionsFocus::FlashBrightness => {
let delta = if key.code == KeyCode::Left { -0.1 } else { 0.1 };
ctx.app.ui.flash_brightness =
(ctx.app.ui.flash_brightness + delta).clamp(0.0, 1.0);
ctx.dispatch(AppCommand::AdjustFlashBrightness(delta));
}
OptionsFocus::LinkEnabled => ctx.link.set_enabled(!ctx.link.is_enabled()),
OptionsFocus::StartStopSync => ctx
@@ -1270,8 +1313,10 @@ fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
let delta = if key.code == KeyCode::Left { -1.0 } else { 1.0 };
ctx.link.set_quantum(ctx.link.quantum() + delta);
}
OptionsFocus::MidiOutput0 | OptionsFocus::MidiOutput1 |
OptionsFocus::MidiOutput2 | OptionsFocus::MidiOutput3 => {
OptionsFocus::MidiOutput0
| OptionsFocus::MidiOutput1
| OptionsFocus::MidiOutput2
| OptionsFocus::MidiOutput3 => {
let slot = match ctx.app.options.focus {
OptionsFocus::MidiOutput0 => 0,
OptionsFocus::MidiOutput1 => 1,
@@ -1285,7 +1330,12 @@ fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
.enumerate()
.filter(|(idx, _)| {
ctx.app.midi.selected_outputs[slot] == Some(*idx)
|| !ctx.app.midi.selected_outputs.iter().enumerate()
|| !ctx
.app
.midi
.selected_outputs
.iter()
.enumerate()
.any(|(s, sel)| s != slot && *sel == Some(*idx))
})
.collect();
@@ -1295,7 +1345,11 @@ fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
.map(|p| p + 1)
.unwrap_or(0);
let new_pos = if key.code == KeyCode::Left {
if current_pos == 0 { total_options - 1 } else { current_pos - 1 }
if current_pos == 0 {
total_options - 1
} else {
current_pos - 1
}
} else {
(current_pos + 1) % total_options
};
@@ -1308,13 +1362,16 @@ fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
let (device_idx, device) = available[new_pos - 1];
if ctx.app.midi.connect_output(slot, device_idx).is_ok() {
ctx.dispatch(AppCommand::SetStatus(format!(
"MIDI output {}: {}", slot, device.name
"MIDI output {}: {}",
slot, device.name
)));
}
}
}
OptionsFocus::MidiInput0 | OptionsFocus::MidiInput1 |
OptionsFocus::MidiInput2 | OptionsFocus::MidiInput3 => {
OptionsFocus::MidiInput0
| OptionsFocus::MidiInput1
| OptionsFocus::MidiInput2
| OptionsFocus::MidiInput3 => {
let slot = match ctx.app.options.focus {
OptionsFocus::MidiInput0 => 0,
OptionsFocus::MidiInput1 => 1,
@@ -1328,7 +1385,12 @@ fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
.enumerate()
.filter(|(idx, _)| {
ctx.app.midi.selected_inputs[slot] == Some(*idx)
|| !ctx.app.midi.selected_inputs.iter().enumerate()
|| !ctx
.app
.midi
.selected_inputs
.iter()
.enumerate()
.any(|(s, sel)| s != slot && *sel == Some(*idx))
})
.collect();
@@ -1338,7 +1400,11 @@ fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
.map(|p| p + 1)
.unwrap_or(0);
let new_pos = if key.code == KeyCode::Left {
if current_pos == 0 { total_options - 1 } else { current_pos - 1 }
if current_pos == 0 {
total_options - 1
} else {
current_pos - 1
}
} else {
(current_pos + 1) % total_options
};
@@ -1351,7 +1417,8 @@ fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
let (device_idx, device) = available[new_pos - 1];
if ctx.app.midi.connect_input(slot, device_idx).is_ok() {
ctx.dispatch(AppCommand::SetStatus(format!(
"MIDI input {}: {}", slot, device.name
"MIDI input {}: {}",
slot, device.name
)));
}
}

View File

@@ -146,7 +146,7 @@ fn main() -> io::Result<()> {
audio_sample_pos: Arc::clone(&audio_sample_pos),
sample_rate: Arc::clone(&sample_rate_shared),
lookahead_ms: Arc::clone(&lookahead_ms),
cc_memory: Some(Arc::clone(&app.midi.cc_memory)),
cc_access: Some(Arc::new(app.midi.cc_memory.clone()) as Arc<dyn crate::model::CcAccess>),
#[cfg(feature = "desktop")]
mouse_x: Arc::clone(&mouse_x),
#[cfg(feature = "desktop")]
@@ -257,22 +257,48 @@ fn main() -> io::Result<()> {
// Process pending MIDI commands
while let Ok(midi_cmd) = midi_rx.try_recv() {
match midi_cmd {
engine::MidiCommand::NoteOn { device, channel, note, velocity } => {
engine::MidiCommand::NoteOn {
device,
channel,
note,
velocity,
} => {
app.midi.send_note_on(device, channel, note, velocity);
}
engine::MidiCommand::NoteOff { device, channel, note } => {
engine::MidiCommand::NoteOff {
device,
channel,
note,
} => {
app.midi.send_note_off(device, channel, note);
}
engine::MidiCommand::CC { device, channel, cc, value } => {
engine::MidiCommand::CC {
device,
channel,
cc,
value,
} => {
app.midi.send_cc(device, channel, cc, value);
}
engine::MidiCommand::PitchBend { device, channel, value } => {
engine::MidiCommand::PitchBend {
device,
channel,
value,
} => {
app.midi.send_pitch_bend(device, channel, value);
}
engine::MidiCommand::Pressure { device, channel, value } => {
engine::MidiCommand::Pressure {
device,
channel,
value,
} => {
app.midi.send_pressure(device, channel, value);
}
engine::MidiCommand::ProgramChange { device, channel, program } => {
engine::MidiCommand::ProgramChange {
device,
channel,
program,
} => {
app.midi.send_program_change(device, channel, program);
}
engine::MidiCommand::Clock { device } => app.midi.send_realtime(device, 0xF8),

View File

@@ -2,10 +2,53 @@ use std::sync::{Arc, Mutex};
use midir::{MidiInput, MidiOutput};
use crate::model::CcMemory;
use cagire_forth::CcAccess;
pub const MAX_MIDI_OUTPUTS: usize = 4;
pub const MAX_MIDI_INPUTS: usize = 4;
pub const MAX_MIDI_DEVICES: usize = 4;
/// Raw CC memory storage type
type CcMemoryInner = Arc<Mutex<[[[u8; 128]; 16]; MAX_MIDI_DEVICES]>>;
/// CC memory storage: [device][channel][cc_number] -> value
/// Wrapped in a newtype to implement CcAccess (orphan rule)
#[derive(Clone)]
pub struct CcMemory(CcMemoryInner);
impl CcMemory {
pub fn new() -> Self {
Self(Arc::new(Mutex::new([[[0u8; 128]; 16]; MAX_MIDI_DEVICES])))
}
fn inner(&self) -> &CcMemoryInner {
&self.0
}
/// Set a CC value (for testing)
#[allow(dead_code)]
pub fn set_cc(&self, device: usize, channel: usize, cc: usize, value: u8) {
if let Ok(mut mem) = self.0.lock() {
mem[device.min(MAX_MIDI_DEVICES - 1)][channel.min(15)][cc.min(127)] = value;
}
}
}
impl Default for CcMemory {
fn default() -> Self {
Self::new()
}
}
impl CcAccess for CcMemory {
fn get_cc(&self, device: usize, channel: usize, cc: usize) -> u8 {
self.0
.lock()
.ok()
.map(|mem| mem[device.min(MAX_MIDI_DEVICES - 1)][channel.min(15)][cc.min(127)])
.unwrap_or(0)
}
}
#[derive(Clone, Debug)]
pub struct MidiDeviceInfo {
@@ -46,7 +89,7 @@ pub fn list_midi_inputs() -> Vec<MidiDeviceInfo> {
pub struct MidiState {
output_conns: [Option<midir::MidiOutputConnection>; MAX_MIDI_OUTPUTS],
input_conns: [Option<midir::MidiInputConnection<(CcMemory, usize)>>; MAX_MIDI_INPUTS],
input_conns: [Option<midir::MidiInputConnection<(CcMemoryInner, usize)>>; MAX_MIDI_INPUTS],
pub selected_outputs: [Option<usize>; MAX_MIDI_OUTPUTS],
pub selected_inputs: [Option<usize>; MAX_MIDI_INPUTS],
pub cc_memory: CcMemory,
@@ -65,7 +108,7 @@ impl MidiState {
input_conns: [None, None, None, None],
selected_outputs: [None; MAX_MIDI_OUTPUTS],
selected_inputs: [None; MAX_MIDI_INPUTS],
cc_memory: Arc::new(Mutex::new([[[0u8; 128]; 16]; MAX_MIDI_OUTPUTS])),
cc_memory: CcMemory::new(),
}
}
@@ -99,7 +142,7 @@ impl MidiState {
let ports = midi_in.ports();
let port = ports.get(port_index).ok_or("MIDI input port not found")?;
let cc_mem = Arc::clone(&self.cc_memory);
let cc_mem = Arc::clone(self.cc_memory.inner());
let conn = midi_in
.connect(
port,
@@ -133,60 +176,46 @@ impl MidiState {
}
}
pub fn send_note_on(&mut self, device: u8, channel: u8, note: u8, velocity: u8) {
fn send_message(&mut self, device: u8, message: &[u8]) {
let slot = (device as usize).min(MAX_MIDI_OUTPUTS - 1);
if let Some(conn) = &mut self.output_conns[slot] {
let status = 0x90 | (channel & 0x0F);
let _ = conn.send(&[status, note & 0x7F, velocity & 0x7F]);
let _ = conn.send(message);
}
}
pub fn send_note_on(&mut self, device: u8, channel: u8, note: u8, velocity: u8) {
let status = 0x90 | (channel & 0x0F);
self.send_message(device, &[status, note & 0x7F, velocity & 0x7F]);
}
pub fn send_note_off(&mut self, device: u8, channel: u8, note: u8) {
let slot = (device as usize).min(MAX_MIDI_OUTPUTS - 1);
if let Some(conn) = &mut self.output_conns[slot] {
let status = 0x80 | (channel & 0x0F);
let _ = conn.send(&[status, note & 0x7F, 0]);
}
let status = 0x80 | (channel & 0x0F);
self.send_message(device, &[status, note & 0x7F, 0]);
}
pub fn send_cc(&mut self, device: u8, channel: u8, cc: u8, value: u8) {
let slot = (device as usize).min(MAX_MIDI_OUTPUTS - 1);
if let Some(conn) = &mut self.output_conns[slot] {
let status = 0xB0 | (channel & 0x0F);
let _ = conn.send(&[status, cc & 0x7F, value & 0x7F]);
}
let status = 0xB0 | (channel & 0x0F);
self.send_message(device, &[status, cc & 0x7F, value & 0x7F]);
}
pub fn send_pitch_bend(&mut self, device: u8, channel: u8, value: u16) {
let slot = (device as usize).min(MAX_MIDI_OUTPUTS - 1);
if let Some(conn) = &mut self.output_conns[slot] {
let status = 0xE0 | (channel & 0x0F);
let lsb = (value & 0x7F) as u8;
let msb = ((value >> 7) & 0x7F) as u8;
let _ = conn.send(&[status, lsb, msb]);
}
let status = 0xE0 | (channel & 0x0F);
let lsb = (value & 0x7F) as u8;
let msb = ((value >> 7) & 0x7F) as u8;
self.send_message(device, &[status, lsb, msb]);
}
pub fn send_pressure(&mut self, device: u8, channel: u8, value: u8) {
let slot = (device as usize).min(MAX_MIDI_OUTPUTS - 1);
if let Some(conn) = &mut self.output_conns[slot] {
let status = 0xD0 | (channel & 0x0F);
let _ = conn.send(&[status, value & 0x7F]);
}
let status = 0xD0 | (channel & 0x0F);
self.send_message(device, &[status, value & 0x7F]);
}
pub fn send_program_change(&mut self, device: u8, channel: u8, program: u8) {
let slot = (device as usize).min(MAX_MIDI_OUTPUTS - 1);
if let Some(conn) = &mut self.output_conns[slot] {
let status = 0xC0 | (channel & 0x0F);
let _ = conn.send(&[status, program & 0x7F]);
}
let status = 0xC0 | (channel & 0x0F);
self.send_message(device, &[status, program & 0x7F]);
}
pub fn send_realtime(&mut self, device: u8, msg: u8) {
let slot = (device as usize).min(MAX_MIDI_OUTPUTS - 1);
if let Some(conn) = &mut self.output_conns[slot] {
let _ = conn.send(&[msg]);
}
self.send_message(device, &[msg]);
}
}

View File

@@ -5,4 +5,7 @@ pub use cagire_project::{
load, save, Bank, LaunchQuantization, Pattern, PatternSpeed, Project, SyncMode, MAX_BANKS,
MAX_PATTERNS,
};
pub use script::{CcMemory, Dictionary, ExecutionTrace, Rng, ScriptEngine, SourceSpan, StepContext, Value, Variables};
pub use script::{
CcAccess, Dictionary, ExecutionTrace, Rng, ScriptEngine, SourceSpan, StepContext, Value,
Variables,
};

View File

@@ -1,6 +1,8 @@
use cagire_forth::Forth;
pub use cagire_forth::{CcMemory, Dictionary, ExecutionTrace, Rng, SourceSpan, StepContext, Value, Variables};
pub use cagire_forth::{
CcAccess, Dictionary, ExecutionTrace, Rng, SourceSpan, StepContext, Value, Variables,
};
pub struct ScriptEngine {
forth: Forth,

View File

@@ -1,65 +1,47 @@
use serde::{Deserialize, Serialize};
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use crate::theme::ThemeColors;
use crate::theme::{ThemeColors, THEMES};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum ColorScheme {
#[default]
CatppuccinMocha,
CatppuccinLatte,
Nord,
Dracula,
GruvboxDark,
Monokai,
PitchBlack,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ColorScheme(usize);
impl ColorScheme {
pub fn label(self) -> &'static str {
match self {
Self::CatppuccinMocha => "Catppuccin Mocha",
Self::CatppuccinLatte => "Catppuccin Latte",
Self::Nord => "Nord",
Self::Dracula => "Dracula",
Self::GruvboxDark => "Gruvbox Dark",
Self::Monokai => "Monokai",
Self::PitchBlack => "Pitch Black",
}
THEMES[self.0].label
}
pub fn next(self) -> Self {
match self {
Self::CatppuccinMocha => Self::CatppuccinLatte,
Self::CatppuccinLatte => Self::Nord,
Self::Nord => Self::Dracula,
Self::Dracula => Self::GruvboxDark,
Self::GruvboxDark => Self::Monokai,
Self::Monokai => Self::PitchBlack,
Self::PitchBlack => Self::CatppuccinMocha,
}
Self((self.0 + 1) % THEMES.len())
}
pub fn prev(self) -> Self {
match self {
Self::CatppuccinMocha => Self::PitchBlack,
Self::CatppuccinLatte => Self::CatppuccinMocha,
Self::Nord => Self::CatppuccinLatte,
Self::Dracula => Self::Nord,
Self::GruvboxDark => Self::Dracula,
Self::Monokai => Self::GruvboxDark,
Self::PitchBlack => Self::Monokai,
}
Self((self.0 + THEMES.len() - 1) % THEMES.len())
}
pub fn to_theme(self) -> ThemeColors {
match self {
Self::CatppuccinMocha => ThemeColors::catppuccin_mocha(),
Self::CatppuccinLatte => ThemeColors::catppuccin_latte(),
Self::Nord => ThemeColors::nord(),
Self::Dracula => ThemeColors::dracula(),
Self::GruvboxDark => ThemeColors::gruvbox_dark(),
Self::Monokai => ThemeColors::monokai(),
Self::PitchBlack => ThemeColors::pitch_black(),
}
(THEMES[self.0].colors)()
}
}
impl Serialize for ColorScheme {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(THEMES[self.0].id)
}
}
impl<'de> Deserialize<'de> for ColorScheme {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
THEMES
.iter()
.position(|t| t.id == s)
.map(ColorScheme)
.ok_or_else(|| de::Error::custom(format!("unknown theme: {s}")))
}
}

View File

@@ -12,7 +12,6 @@ use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Cell, Clear, Paragraph, Row, Table};
use ratatui::Frame;
use cagire_forth::Forth;
use crate::app::App;
use crate::engine::{LinkState, SequencerSnapshot};
use crate::model::{SourceSpan, StepContext, Value};
@@ -23,12 +22,17 @@ use crate::views::highlight::{self, highlight_line, highlight_line_with_runtime}
use crate::widgets::{
ConfirmModal, ModalFrame, NavMinimap, NavTile, SampleBrowser, TextInputModal,
};
use cagire_forth::Forth;
use super::{
dict_view, engine_view, help_view, main_view, options_view, patterns_view, title_view,
};
fn compute_stack_display(lines: &[String], editor: &cagire_ratatui::Editor, cache: &std::cell::RefCell<Option<StackCache>>) -> String {
fn compute_stack_display(
lines: &[String],
editor: &cagire_ratatui::Editor,
cache: &std::cell::RefCell<Option<StackCache>>,
) -> String {
let cursor_line = editor.cursor().0;
let mut hasher = DefaultHasher::new();
@@ -46,7 +50,11 @@ fn compute_stack_display(lines: &[String], editor: &cagire_ratatui::Editor, cach
}
}
let partial: Vec<&str> = lines.iter().take(cursor_line + 1).map(|s| s.as_str()).collect();
let partial: Vec<&str> = lines
.iter()
.take(cursor_line + 1)
.map(|s| s.as_str())
.collect();
let script = partial.join("\n");
let result = if script.trim().is_empty() {
@@ -70,7 +78,7 @@ fn compute_stack_display(lines: &[String], editor: &cagire_ratatui::Editor, cach
speed: 1.0,
fill: false,
nudge_secs: 0.0,
cc_memory: None,
cc_access: None,
#[cfg(feature = "desktop")]
mouse_x: 0.5,
#[cfg(feature = "desktop")]
@@ -240,7 +248,11 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, snapshot: &Sequenc
}
fn header_height(width: u16) -> u16 {
if width >= 80 { 1 } else { 2 }
if width >= 80 {
1
} else {
2
}
}
fn render_side_panel(frame: &mut Frame, app: &App, area: Rect) {
@@ -284,11 +296,8 @@ fn render_header(
.areas(area);
(t, l, tp, b, p, s)
} else {
let [line1, line2] = Layout::vertical([
Constraint::Length(1),
Constraint::Length(1),
])
.areas(area);
let [line1, line2] =
Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(area);
let [t, l, tp, s] = Layout::horizontal([
Constraint::Min(12),
@@ -298,11 +307,8 @@ fn render_header(
])
.areas(line1);
let [b, p] = Layout::horizontal([
Constraint::Fill(1),
Constraint::Fill(2),
])
.areas(line2);
let [b, p] =
Layout::horizontal([Constraint::Fill(1), Constraint::Fill(2)]).areas(line2);
(t, l, tp, b, p, s)
};
@@ -323,7 +329,11 @@ fn render_header(
// Fill indicator
let fill = app.live_keys.fill();
let fill_fg = if fill { theme.status.fill_on } else { theme.status.fill_off };
let fill_fg = if fill {
theme.status.fill_on
} else {
theme.status.fill_off
};
let fill_style = Style::new().bg(theme.status.fill_bg).fg(fill_fg);
frame.render_widget(
Paragraph::new(if fill { "F" } else { "·" })
@@ -350,7 +360,9 @@ fn render_header(
.as_deref()
.map(|n| format!(" {n} "))
.unwrap_or_else(|| format!(" Bank {:02} ", app.editor_ctx.bank + 1));
let bank_style = Style::new().bg(theme.header.bank_bg).fg(theme.ui.text_primary);
let bank_style = Style::new()
.bg(theme.header.bank_bg)
.fg(theme.ui.text_primary);
frame.render_widget(
Paragraph::new(bank_name)
.style(bank_style)
@@ -381,7 +393,9 @@ fn render_header(
" {} · {} steps{}{}{} ",
pattern_name, pattern.length, speed_info, page_info, iter_info
);
let pattern_style = Style::new().bg(theme.header.pattern_bg).fg(theme.ui.text_primary);
let pattern_style = Style::new()
.bg(theme.header.pattern_bg)
.fg(theme.ui.text_primary);
frame.render_widget(
Paragraph::new(pattern_text)
.style(pattern_style)
@@ -394,7 +408,9 @@ fn render_header(
let peers = link.peers();
let voices = app.metrics.active_voices;
let stats_text = format!(" CPU {cpu_pct:.0}% V:{voices} L:{peers} ");
let stats_style = Style::new().bg(theme.header.stats_bg).fg(theme.header.stats_fg);
let stats_style = Style::new()
.bg(theme.header.stats_bg)
.fg(theme.header.stats_fg);
frame.render_widget(
Paragraph::new(stats_text)
.style(stats_style)
@@ -528,11 +544,12 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
ConfirmModal::new("Confirm", &format!("Delete step {}?", step + 1), *selected)
.render_centered(frame, term);
}
Modal::ConfirmDeleteSteps { steps, selected, .. } => {
Modal::ConfirmDeleteSteps {
steps, selected, ..
} => {
let nums: Vec<String> = steps.iter().map(|s| format!("{:02}", s + 1)).collect();
let label = format!("Delete steps {}?", nums.join(", "));
ConfirmModal::new("Confirm", &label, *selected)
.render_centered(frame, term);
ConfirmModal::new("Confirm", &label, *selected).render_centered(frame, term);
}
Modal::ConfirmResetPattern {
pattern, selected, ..
@@ -637,7 +654,9 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
let step_name = step.and_then(|s| s.name.as_ref());
let title = match (source_idx, step_name) {
(Some(src), Some(name)) => format!("Step {:02}: {}{:02}", step_idx + 1, name, src + 1),
(Some(src), Some(name)) => {
format!("Step {:02}: {}{:02}", step_idx + 1, name, src + 1)
}
(None, Some(name)) => format!("Step {:02}: {}", step_idx + 1, name),
(Some(src), None) => format!("Step {:02}{:02}", step_idx + 1, src + 1),
(None, None) => format!("Step {:02}", step_idx + 1),
@@ -816,7 +835,11 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
]);
frame.render_widget(Paragraph::new(hint).alignment(Alignment::Right), hint_area);
} else if app.editor_ctx.show_stack {
let stack_text = compute_stack_display(text_lines, &app.editor_ctx.editor, &app.editor_ctx.stack_cache);
let stack_text = compute_stack_display(
text_lines,
&app.editor_ctx.editor,
&app.editor_ctx.stack_cache,
);
let hint = Line::from(vec![
Span::styled("Esc", key),
Span::styled(" save ", dim),
@@ -910,7 +933,9 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
Style::default()
.fg(theme.hint.key)
.add_modifier(Modifier::BOLD),
Style::default().fg(theme.ui.text_primary).bg(theme.ui.surface),
Style::default()
.fg(theme.ui.text_primary)
.bg(theme.ui.surface),
)
} else {
(
@@ -962,7 +987,11 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
.skip(*scroll)
.take(visible_rows)
.map(|(i, (key, name, desc))| {
let bg = if i % 2 == 0 { theme.table.row_even } else { theme.table.row_odd };
let bg = if i % 2 == 0 {
theme.table.row_even
} else {
theme.table.row_odd
};
Row::new(vec![
Cell::from(*key).style(Style::default().fg(theme.modal.confirm)),
Cell::from(*name).style(Style::default().fg(theme.modal.input)),
@@ -1004,7 +1033,10 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
Span::styled("Esc/?", Style::default().fg(theme.hint.key)),
Span::styled(" close", Style::default().fg(theme.hint.text)),
]);
frame.render_widget(Paragraph::new(keybind_hint).alignment(Alignment::Right), hint_area);
frame.render_widget(
Paragraph::new(keybind_hint).alignment(Alignment::Right),
hint_area,
);
}
}
}