Clean plugins
Some checks failed
Deploy Website / deploy (push) Failing after 4m48s

This commit is contained in:
2026-02-21 01:27:32 +01:00
parent 75a8fd4401
commit baa2aba381
12 changed files with 284 additions and 167 deletions

View File

@@ -214,6 +214,11 @@ pub fn create_editor(
let shared = editor.bridge.shared_state.load(); let shared = editor.bridge.shared_state.load();
editor.snapshot = SequencerSnapshot::from(shared.as_ref()); editor.snapshot = SequencerSnapshot::from(shared.as_ref());
// Sync host tempo into LinkState so title bar shows real tempo
if shared.tempo > 0.0 {
editor.link.set_tempo(shared.tempo);
}
// Feed scope and spectrum data into app metrics // Feed scope and spectrum data into app metrics
editor.app.metrics.scope = editor.bridge.scope_buffer.read(); editor.app.metrics.scope = editor.bridge.scope_buffer.read();
(editor.app.metrics.peak_left, editor.app.metrics.peak_right) = (editor.app.metrics.peak_left, editor.app.metrics.peak_right) =

View File

@@ -91,6 +91,7 @@ impl App {
Self::build(variables, dict, rng, false) Self::build(variables, dict, rng, false)
} }
#[allow(dead_code)]
pub fn new_plugin(variables: Variables, dict: Dictionary, rng: Rng) -> Self { pub fn new_plugin(variables: Variables, dict: Dictionary, rng: Rng) -> Self {
Self::build(variables, dict, rng, true) Self::build(variables, dict, rng, true)
} }

View File

@@ -158,12 +158,17 @@ pub struct SharedSequencerState {
pub active_patterns: Vec<ActivePatternState>, pub active_patterns: Vec<ActivePatternState>,
pub step_traces: Arc<StepTracesMap>, pub step_traces: Arc<StepTracesMap>,
pub event_count: usize, pub event_count: usize,
pub tempo: f64,
pub beat: f64,
} }
#[allow(dead_code)]
pub struct SequencerSnapshot { pub struct SequencerSnapshot {
pub active_patterns: Vec<ActivePatternState>, pub active_patterns: Vec<ActivePatternState>,
step_traces: Arc<StepTracesMap>, step_traces: Arc<StepTracesMap>,
pub event_count: usize, pub event_count: usize,
pub tempo: f64,
pub beat: f64,
} }
impl From<&SharedSequencerState> for SequencerSnapshot { impl From<&SharedSequencerState> for SequencerSnapshot {
@@ -172,6 +177,8 @@ impl From<&SharedSequencerState> for SequencerSnapshot {
active_patterns: s.active_patterns.clone(), active_patterns: s.active_patterns.clone(),
step_traces: Arc::clone(&s.step_traces), step_traces: Arc::clone(&s.step_traces),
event_count: s.event_count, event_count: s.event_count,
tempo: s.tempo,
beat: s.beat,
} }
} }
} }
@@ -183,6 +190,8 @@ impl SequencerSnapshot {
active_patterns: Vec::new(), active_patterns: Vec::new(),
step_traces: Arc::new(HashMap::new()), step_traces: Arc::new(HashMap::new()),
event_count: 0, event_count: 0,
tempo: 0.0,
beat: 0.0,
} }
} }
@@ -222,11 +231,7 @@ pub struct SequencerHandle {
impl SequencerHandle { impl SequencerHandle {
pub fn snapshot(&self) -> SequencerSnapshot { pub fn snapshot(&self) -> SequencerSnapshot {
let state = self.shared_state.load(); let state = self.shared_state.load();
SequencerSnapshot { SequencerSnapshot::from(state.as_ref())
active_patterns: state.active_patterns.clone(),
step_traces: Arc::clone(&state.step_traces),
event_count: state.event_count,
}
} }
pub fn swap_audio_channel(&self) -> Receiver<AudioCommand> { pub fn swap_audio_channel(&self) -> Receiver<AudioCommand> {
@@ -563,6 +568,8 @@ pub struct SequencerState {
cc_access: Option<Arc<dyn CcAccess>>, cc_access: Option<Arc<dyn CcAccess>>,
muted: std::collections::HashSet<(usize, usize)>, muted: std::collections::HashSet<(usize, usize)>,
soloed: std::collections::HashSet<(usize, usize)>, soloed: std::collections::HashSet<(usize, usize)>,
last_tempo: f64,
last_beat: f64,
} }
impl SequencerState { impl SequencerState {
@@ -592,6 +599,8 @@ impl SequencerState {
cc_access, cc_access,
muted: std::collections::HashSet::new(), muted: std::collections::HashSet::new(),
soloed: std::collections::HashSet::new(), soloed: std::collections::HashSet::new(),
last_tempo: 0.0,
last_beat: 0.0,
} }
} }
@@ -701,6 +710,8 @@ impl SequencerState {
pub fn tick(&mut self, input: TickInput) -> TickOutput { pub fn tick(&mut self, input: TickInput) -> TickOutput {
self.process_commands(input.commands); self.process_commands(input.commands);
self.last_tempo = input.tempo;
self.last_beat = input.beat;
if !input.playing { if !input.playing {
return self.tick_paused(); return self.tick_paused();
@@ -1092,6 +1103,8 @@ impl SequencerState {
.collect(), .collect(),
step_traces: Arc::clone(&self.step_traces), step_traces: Arc::clone(&self.step_traces),
event_count: self.event_count, event_count: self.event_count,
tempo: self.last_tempo,
beat: self.last_beat,
} }
} }
} }

View File

@@ -169,7 +169,7 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input
KeyCode::Char('?') => { KeyCode::Char('?') => {
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 })); ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
} }
KeyCode::Char(' ') => { KeyCode::Char(' ') if !ctx.app.plugin_mode => {
ctx.dispatch(AppCommand::TogglePlaying); ctx.dispatch(AppCommand::TogglePlaying);
ctx.playing ctx.playing
.store(ctx.app.playback.playing, Ordering::Relaxed); .store(ctx.app.playback.playing, Ordering::Relaxed);

View File

@@ -77,7 +77,7 @@ pub(super) fn handle_help_page(ctx: &mut InputContext, key: KeyEvent) -> InputRe
KeyCode::Char('?') => { KeyCode::Char('?') => {
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 })); ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
} }
KeyCode::Char(' ') => { KeyCode::Char(' ') if !ctx.app.plugin_mode => {
ctx.dispatch(AppCommand::TogglePlaying); ctx.dispatch(AppCommand::TogglePlaying);
ctx.playing ctx.playing
.store(ctx.app.playback.playing, Ordering::Relaxed); .store(ctx.app.playback.playing, Ordering::Relaxed);
@@ -247,7 +247,7 @@ pub(super) fn handle_dict_page(ctx: &mut InputContext, key: KeyEvent) -> InputRe
KeyCode::Char('?') => { KeyCode::Char('?') => {
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 })); ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
} }
KeyCode::Char(' ') => { KeyCode::Char(' ') if !ctx.app.plugin_mode => {
ctx.dispatch(AppCommand::TogglePlaying); ctx.dispatch(AppCommand::TogglePlaying);
ctx.playing ctx.playing
.store(ctx.app.playback.playing, Ordering::Relaxed); .store(ctx.app.playback.playing, Ordering::Relaxed);

View File

@@ -29,7 +29,7 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool
selected: false, selected: false,
})); }));
} }
KeyCode::Char(' ') => { KeyCode::Char(' ') if !ctx.app.plugin_mode => {
ctx.dispatch(AppCommand::TogglePlaying); ctx.dispatch(AppCommand::TogglePlaying);
ctx.playing ctx.playing
.store(ctx.app.playback.playing, Ordering::Relaxed); .store(ctx.app.playback.playing, Ordering::Relaxed);
@@ -103,9 +103,11 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool
} }
KeyCode::Char('h') if ctrl => ctx.dispatch(AppCommand::HardenSteps), KeyCode::Char('h') if ctrl => ctx.dispatch(AppCommand::HardenSteps),
KeyCode::Char('l') => super::open_load(ctx), KeyCode::Char('l') => super::open_load(ctx),
KeyCode::Char('+') | KeyCode::Char('=') => ctx.dispatch(AppCommand::TempoUp), KeyCode::Char('+') | KeyCode::Char('=') if !ctx.app.plugin_mode => {
KeyCode::Char('-') => ctx.dispatch(AppCommand::TempoDown), ctx.dispatch(AppCommand::TempoUp);
KeyCode::Char('T') => { }
KeyCode::Char('-') if !ctx.app.plugin_mode => ctx.dispatch(AppCommand::TempoDown),
KeyCode::Char('T') if !ctx.app.plugin_mode => {
let current = format!("{:.1}", ctx.link.tempo()); let current = format!("{:.1}", ctx.link.tempo());
ctx.dispatch(AppCommand::OpenModal(Modal::SetTempo(current))); ctx.dispatch(AppCommand::OpenModal(Modal::SetTempo(current)));
} }

View File

@@ -677,7 +677,7 @@ fn handle_options_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect)
let focus = ctx.app.options.focus; let focus = ctx.app.options.focus;
let plugin_mode = ctx.app.plugin_mode; let plugin_mode = ctx.app.plugin_mode;
let focus_line = focus.line_index(plugin_mode); let focus_line = focus.line_index(plugin_mode);
let total_lines = if plugin_mode { 43 } else { 40 }; let total_lines = crate::state::options::total_lines(plugin_mode);
let max_visible = padded.height as usize; let max_visible = padded.height as usize;
let scroll_offset = if total_lines <= max_visible { let scroll_offset = if total_lines <= max_visible {

View File

@@ -210,7 +210,7 @@ pub(super) fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> Inpu
KeyCode::Left | KeyCode::Right => { KeyCode::Left | KeyCode::Right => {
cycle_option_value(ctx, key.code == KeyCode::Right); cycle_option_value(ctx, key.code == KeyCode::Right);
} }
KeyCode::Char(' ') => { KeyCode::Char(' ') if !ctx.app.plugin_mode => {
ctx.dispatch(AppCommand::TogglePlaying); ctx.dispatch(AppCommand::TogglePlaying);
ctx.playing ctx.playing
.store(ctx.app.playback.playing, Ordering::Relaxed); .store(ctx.app.playback.playing, Ordering::Relaxed);

View File

@@ -84,7 +84,7 @@ pub(super) fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> Inp
} }
} }
} }
KeyCode::Char(' ') => { KeyCode::Char(' ') if !ctx.app.plugin_mode => {
ctx.dispatch(AppCommand::TogglePlaying); ctx.dispatch(AppCommand::TogglePlaying);
ctx.playing ctx.playing
.store(ctx.app.playback.playing, Ordering::Relaxed); .store(ctx.app.playback.playing, Ordering::Relaxed);

View File

@@ -56,9 +56,31 @@ impl CyclicEnum for OptionsFocus {
]; ];
} }
// Line indices when Font/ZoomFactor are shown (plugin mode). const PLUGIN_ONLY: &[OptionsFocus] = &[
// In terminal mode, Font/ZoomFactor are absent; all lines after ShowPreview shift up by 2. OptionsFocus::Font,
const FOCUS_LINES: &[(OptionsFocus, usize)] = &[ OptionsFocus::ZoomFactor,
OptionsFocus::WindowSize,
];
const STANDALONE_ONLY: &[OptionsFocus] = &[
OptionsFocus::LinkEnabled,
OptionsFocus::StartStopSync,
OptionsFocus::Quantum,
OptionsFocus::MidiOutput0,
OptionsFocus::MidiOutput1,
OptionsFocus::MidiOutput2,
OptionsFocus::MidiOutput3,
OptionsFocus::MidiInput0,
OptionsFocus::MidiInput1,
OptionsFocus::MidiInput2,
OptionsFocus::MidiInput3,
];
/// Section layout: header line, divider line, then option lines.
/// Each entry gives the raw line offsets assuming ALL sections are visible
/// (plugin mode with Font/Zoom/Window shown).
const FULL_LAYOUT: &[(OptionsFocus, usize)] = &[
// DISPLAY section: header=0, divider=1
(OptionsFocus::ColorScheme, 2), (OptionsFocus::ColorScheme, 2),
(OptionsFocus::HueRotation, 3), (OptionsFocus::HueRotation, 3),
(OptionsFocus::RefreshRate, 4), (OptionsFocus::RefreshRate, 4),
@@ -70,51 +92,112 @@ const FOCUS_LINES: &[(OptionsFocus, usize)] = &[
(OptionsFocus::Font, 10), (OptionsFocus::Font, 10),
(OptionsFocus::ZoomFactor, 11), (OptionsFocus::ZoomFactor, 11),
(OptionsFocus::WindowSize, 12), (OptionsFocus::WindowSize, 12),
// blank=13, ABLETON LINK header=14, divider=15
(OptionsFocus::LinkEnabled, 16), (OptionsFocus::LinkEnabled, 16),
(OptionsFocus::StartStopSync, 17), (OptionsFocus::StartStopSync, 17),
(OptionsFocus::Quantum, 18), (OptionsFocus::Quantum, 18),
// blank=19, SESSION header=20, divider=21, Tempo=22, Beat=23, Phase=24
// blank=25, MIDI OUTPUTS header=26, divider=27
(OptionsFocus::MidiOutput0, 28), (OptionsFocus::MidiOutput0, 28),
(OptionsFocus::MidiOutput1, 29), (OptionsFocus::MidiOutput1, 29),
(OptionsFocus::MidiOutput2, 30), (OptionsFocus::MidiOutput2, 30),
(OptionsFocus::MidiOutput3, 31), (OptionsFocus::MidiOutput3, 31),
// blank=32, MIDI INPUTS header=33, divider=34
(OptionsFocus::MidiInput0, 35), (OptionsFocus::MidiInput0, 35),
(OptionsFocus::MidiInput1, 36), (OptionsFocus::MidiInput1, 36),
(OptionsFocus::MidiInput2, 37), (OptionsFocus::MidiInput2, 37),
(OptionsFocus::MidiInput3, 38), (OptionsFocus::MidiInput3, 38),
// blank=39, ONBOARDING header=40, divider=41
(OptionsFocus::ResetOnboarding, 42), (OptionsFocus::ResetOnboarding, 42),
]; ];
const PLUGIN_ONLY: &[OptionsFocus] = &[OptionsFocus::Font, OptionsFocus::ZoomFactor, OptionsFocus::WindowSize];
impl OptionsFocus { impl OptionsFocus {
fn is_plugin_only(self) -> bool { fn is_plugin_only(self) -> bool {
PLUGIN_ONLY.contains(&self) PLUGIN_ONLY.contains(&self)
} }
fn is_standalone_only(self) -> bool {
STANDALONE_ONLY.contains(&self)
}
fn is_visible(self, plugin_mode: bool) -> bool {
if self.is_plugin_only() && !plugin_mode {
return false;
}
if self.is_standalone_only() && plugin_mode {
return false;
}
true
}
pub fn line_index(self, plugin_mode: bool) -> usize { pub fn line_index(self, plugin_mode: bool) -> usize {
let base = FOCUS_LINES visible_layout(plugin_mode)
.iter() .iter()
.find(|(f, _)| *f == self) .find(|(f, _)| *f == self)
.map(|(_, l)| *l) .map(|(_, l)| *l)
.unwrap_or(0); .unwrap_or(0)
if plugin_mode || base <= 9 {
base
} else {
base - 3
}
} }
pub fn at_line(line: usize, plugin_mode: bool) -> Option<OptionsFocus> { pub fn at_line(line: usize, plugin_mode: bool) -> Option<OptionsFocus> {
FOCUS_LINES.iter().find_map(|(f, l)| { visible_layout(plugin_mode)
if f.is_plugin_only() && !plugin_mode { .iter()
return None; .find(|(_, l)| *l == line)
} .map(|(f, _)| *f)
let effective = if plugin_mode || *l <= 9 { *l } else { *l - 3 };
if effective == line { Some(*f) } else { None }
})
} }
} }
/// Total number of rendered lines for the options view.
pub fn total_lines(plugin_mode: bool) -> usize {
visible_layout(plugin_mode)
.last()
.map(|(_, l)| *l + 1)
.unwrap_or(0)
}
/// Compute (focus, line_index) pairs for only the visible options,
/// with line indices adjusted to account for hidden sections.
fn visible_layout(plugin_mode: bool) -> Vec<(OptionsFocus, usize)> {
// Start from the full layout and compute adjusted line numbers.
// Hidden items + their section headers/dividers/blanks shrink the layout.
// We know the exact section structure, so we compute the offset to subtract
// based on which sections are hidden.
let mut offset: usize = 0;
// Font/Zoom/Window lines (10,11,12) hidden when !plugin_mode
if !plugin_mode {
offset += 3; // 3 lines for Font, ZoomFactor, WindowSize
}
// Link + Session + MIDI sections hidden when plugin_mode
// These span from blank(13) through MidiInput3(38) = 26 lines
if plugin_mode {
// blank + LINK header + divider + 3 options + blank + SESSION header + divider + 3 readonlys
// + blank + MIDI OUT header + divider + 4 options + blank + MIDI IN header + divider + 4 options
// = 26 lines (indices 13..=38)
let link_section_lines = 26;
offset += link_section_lines;
}
let mut result = Vec::new();
for &(focus, raw_line) in FULL_LAYOUT {
if !focus.is_visible(plugin_mode) {
continue;
}
// Lines at or below index 9 (ShowPreview) are never shifted
let adjusted = if raw_line <= 9 {
raw_line
} else if !plugin_mode && raw_line <= 12 {
// Font/Zoom/Window — these are hidden, skip
continue;
} else {
raw_line - offset
};
result.push((focus, adjusted));
}
result
}
#[derive(Default)] #[derive(Default)]
pub struct OptionsState { pub struct OptionsState {
pub focus: OptionsFocus, pub focus: OptionsFocus,
@@ -125,7 +208,7 @@ impl OptionsState {
let mut f = self.focus; let mut f = self.focus;
loop { loop {
f = f.next(); f = f.next();
if !f.is_plugin_only() || plugin_mode { if f.is_visible(plugin_mode) {
break; break;
} }
} }
@@ -136,7 +219,7 @@ impl OptionsState {
let mut f = self.focus; let mut f = self.focus;
loop { loop {
f = f.prev(); f = f.prev();
if !f.is_plugin_only() || plugin_mode { if f.is_visible(plugin_mode) {
break; break;
} }
} }

View File

@@ -17,7 +17,9 @@ pub fn bindings_for(page: Page, plugin_mode: bool) -> Vec<(&'static str, &'stati
// Page-specific bindings // Page-specific bindings
match page { match page {
Page::Main => { Page::Main => {
bindings.push(("Space", "Play/Stop", "Toggle playback")); if !plugin_mode {
bindings.push(("Space", "Play/Stop", "Toggle playback"));
}
bindings.push(("←→↑↓", "Navigate", "Move cursor between steps")); bindings.push(("←→↑↓", "Navigate", "Move cursor between steps"));
bindings.push(("Shift+←→↑↓", "Select", "Extend selection")); bindings.push(("Shift+←→↑↓", "Select", "Extend selection"));
bindings.push(("Esc", "Clear", "Clear selection")); bindings.push(("Esc", "Clear", "Clear selection"));
@@ -33,8 +35,10 @@ pub fn bindings_for(page: Page, plugin_mode: bool) -> Vec<(&'static str, &'stati
bindings.push(("Del", "Delete", "Delete step(s)")); bindings.push(("Del", "Delete", "Delete step(s)"));
bindings.push(("< >", "Length", "Decrease/increase pattern length")); bindings.push(("< >", "Length", "Decrease/increase pattern length"));
bindings.push(("[ ]", "Speed", "Decrease/increase pattern speed")); bindings.push(("[ ]", "Speed", "Decrease/increase pattern speed"));
bindings.push(("+ -", "Tempo", "Increase/decrease tempo")); if !plugin_mode {
bindings.push(("T", "Set tempo", "Open tempo input")); bindings.push(("+ -", "Tempo", "Increase/decrease tempo"));
bindings.push(("T", "Set tempo", "Open tempo input"));
}
bindings.push(("L", "Set length", "Open length input")); bindings.push(("L", "Set length", "Open length input"));
bindings.push(("S", "Set speed", "Open speed input")); bindings.push(("S", "Set speed", "Open speed input"));
bindings.push(("f", "Fill", "Toggle fill mode (hold)")); bindings.push(("f", "Fill", "Toggle fill mode (hold)"));
@@ -52,7 +56,9 @@ pub fn bindings_for(page: Page, plugin_mode: bool) -> Vec<(&'static str, &'stati
Page::Patterns => { Page::Patterns => {
bindings.push(("←→↑↓", "Navigate", "Move between banks/patterns")); bindings.push(("←→↑↓", "Navigate", "Move between banks/patterns"));
bindings.push(("Enter", "Select", "Select pattern for editing")); bindings.push(("Enter", "Select", "Select pattern for editing"));
bindings.push(("Space", "Play", "Toggle pattern playback")); if !plugin_mode {
bindings.push(("Space", "Play", "Toggle pattern playback"));
}
bindings.push(("Esc", "Back", "Clear staged or go back")); bindings.push(("Esc", "Back", "Clear staged or go back"));
bindings.push(("c", "Commit", "Commit staged changes")); bindings.push(("c", "Commit", "Commit staged changes"));
bindings.push(("r", "Rename", "Rename bank/pattern")); bindings.push(("r", "Rename", "Rename bank/pattern"));
@@ -72,20 +78,26 @@ pub fn bindings_for(page: Page, plugin_mode: bool) -> Vec<(&'static str, &'stati
bindings.push(("↑↓", "Navigate", "Navigate list items")); bindings.push(("↑↓", "Navigate", "Navigate list items"));
bindings.push(("PgUp/Dn", "Page", "Page through device list")); bindings.push(("PgUp/Dn", "Page", "Page through device list"));
bindings.push(("Enter", "Select", "Select device")); bindings.push(("Enter", "Select", "Select device"));
bindings.push(("R", "Restart", "Restart audio engine")); if !plugin_mode {
bindings.push(("R", "Restart", "Restart audio engine"));
}
bindings.push(("A", "Add path", "Add sample path")); bindings.push(("A", "Add path", "Add sample path"));
bindings.push(("D", "Refresh/Del", "Refresh devices or delete path")); bindings.push(("D", "Refresh/Del", "Refresh devices or delete path"));
bindings.push(("h", "Hush", "Stop all sounds gracefully")); bindings.push(("h", "Hush", "Stop all sounds gracefully"));
bindings.push(("p", "Panic", "Stop all sounds immediately")); bindings.push(("p", "Panic", "Stop all sounds immediately"));
bindings.push(("r", "Reset", "Reset peak voice counter")); bindings.push(("r", "Reset", "Reset peak voice counter"));
bindings.push(("t", "Test", "Play test tone")); if !plugin_mode {
bindings.push(("t", "Test", "Play test tone"));
}
} }
Page::Options => { Page::Options => {
bindings.push(("Tab", "Next", "Move to next option")); bindings.push(("Tab", "Next", "Move to next option"));
bindings.push(("Shift+Tab", "Previous", "Move to previous option")); bindings.push(("Shift+Tab", "Previous", "Move to previous option"));
bindings.push(("↑↓", "Navigate", "Navigate options")); bindings.push(("↑↓", "Navigate", "Navigate options"));
bindings.push(("←→", "Toggle", "Toggle or adjust option")); bindings.push(("←→", "Toggle", "Toggle or adjust option"));
bindings.push(("Space", "Play/Stop", "Toggle playback")); if !plugin_mode {
bindings.push(("Space", "Play/Stop", "Toggle playback"));
}
} }
Page::Help => { Page::Help => {
bindings.push(("↑↓ j/k", "Scroll", "Scroll content")); bindings.push(("↑↓ j/k", "Scroll", "Scroll content"));

View File

@@ -32,84 +32,6 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
let focus = app.options.focus; let focus = app.options.focus;
let content_width = padded.width as usize; let content_width = padded.width as usize;
let enabled = link.is_enabled();
let peers = link.peers();
let (status_text, status_color) = if !enabled {
("DISABLED", theme.link_status.disabled)
} else if peers > 0 {
("CONNECTED", theme.link_status.connected)
} else {
("LISTENING", theme.link_status.listening)
};
let peer_text = if enabled && peers > 0 {
if peers == 1 {
" · 1 peer".to_string()
} else {
format!(" · {peers} peers")
}
} else {
String::new()
};
let link_header = Line::from(vec![
Span::styled(
"ABLETON LINK",
Style::new().fg(theme.ui.header).add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::styled(
status_text,
Style::new().fg(status_color).add_modifier(Modifier::BOLD),
),
Span::styled(peer_text, Style::new().fg(theme.ui.text_muted)),
]);
let quantum_str = format!("{:.0}", link.quantum());
let tempo_str = format!("{:.1} BPM", link.tempo());
let beat_str = format!("{:.2}", link.beat());
let phase_str = format!("{:.2}", link.phase());
let tempo_style = Style::new().fg(theme.values.tempo).add_modifier(Modifier::BOLD);
let value_style = Style::new().fg(theme.values.value);
let midi_outputs = midi::list_midi_outputs();
let midi_inputs = midi::list_midi_inputs();
let midi_out_display = |slot: usize| -> String {
if let Some(idx) = app.midi.selected_outputs[slot] {
midi_outputs
.get(idx)
.map(|d| d.name.clone())
.unwrap_or_else(|| "(disconnected)".to_string())
} else if midi_outputs.is_empty() {
"(none found)".to_string()
} else {
"(not connected)".to_string()
}
};
let midi_in_display = |slot: usize| -> String {
if let Some(idx) = app.midi.selected_inputs[slot] {
midi_inputs
.get(idx)
.map(|d| d.name.clone())
.unwrap_or_else(|| "(disconnected)".to_string())
} else if midi_inputs.is_empty() {
"(none found)".to_string()
} else {
"(not connected)".to_string()
}
};
let midi_out_0 = midi_out_display(0);
let midi_out_1 = midi_out_display(1);
let midi_out_2 = midi_out_display(2);
let midi_out_3 = midi_out_display(3);
let midi_in_0 = midi_in_display(0);
let midi_in_1 = midi_in_display(1);
let midi_in_2 = midi_in_display(2);
let midi_in_3 = midi_in_display(3);
let onboarding_str = format!("{}/6 dismissed", app.ui.onboarding_dismissed.len()); let onboarding_str = format!("{}/6 dismissed", app.ui.onboarding_dismissed.len());
let hue_str = format!("{}°", app.ui.hue_rotation as i32); let hue_str = format!("{}°", app.ui.hue_rotation as i32);
@@ -169,9 +91,9 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
&theme, &theme,
), ),
]; ];
let zoom_str = format!("{:.0}%", app.ui.zoom_factor * 100.0);
let window_str = format!("{}x{}", app.ui.window_width, app.ui.window_height);
if app.plugin_mode { if app.plugin_mode {
let zoom_str = format!("{:.0}%", app.ui.zoom_factor * 100.0);
let window_str = format!("{}x{}", app.ui.window_width, app.ui.window_height);
lines.push(render_option_line( lines.push(render_option_line(
"Font", "Font",
&app.ui.font, &app.ui.font,
@@ -191,48 +113,127 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
&theme, &theme,
)); ));
} }
if !app.plugin_mode {
let enabled = link.is_enabled();
let peers = link.peers();
let (status_text, status_color) = if !enabled {
("DISABLED", theme.link_status.disabled)
} else if peers > 0 {
("CONNECTED", theme.link_status.connected)
} else {
("LISTENING", theme.link_status.listening)
};
let peer_text = if enabled && peers > 0 {
if peers == 1 {
" · 1 peer".to_string()
} else {
format!(" · {peers} peers")
}
} else {
String::new()
};
let link_header = Line::from(vec![
Span::styled(
"ABLETON LINK",
Style::new().fg(theme.ui.header).add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::styled(
status_text,
Style::new().fg(status_color).add_modifier(Modifier::BOLD),
),
Span::styled(peer_text, Style::new().fg(theme.ui.text_muted)),
]);
let quantum_str = format!("{:.0}", link.quantum());
let tempo_str = format!("{:.1} BPM", link.tempo());
let beat_str = format!("{:.2}", link.beat());
let phase_str = format!("{:.2}", link.phase());
let tempo_style = Style::new().fg(theme.values.tempo).add_modifier(Modifier::BOLD);
let value_style = Style::new().fg(theme.values.value);
lines.push(Line::from(""));
lines.extend([
link_header,
render_divider(content_width, &theme),
render_option_line(
"Enabled",
if link.is_enabled() { "On" } else { "Off" },
focus == OptionsFocus::LinkEnabled,
&theme,
),
render_option_line(
"Start/Stop sync",
if link.is_start_stop_sync_enabled() {
"On"
} else {
"Off"
},
focus == OptionsFocus::StartStopSync,
&theme,
),
render_option_line("Quantum", &quantum_str, focus == OptionsFocus::Quantum, &theme),
Line::from(""),
render_section_header("SESSION", &theme),
render_divider(content_width, &theme),
render_readonly_line("Tempo", &tempo_str, tempo_style, &theme),
render_readonly_line("Beat", &beat_str, value_style, &theme),
render_readonly_line("Phase", &phase_str, value_style, &theme),
]);
let midi_outputs = midi::list_midi_outputs();
let midi_inputs = midi::list_midi_inputs();
let midi_out_display = |slot: usize| -> String {
if let Some(idx) = app.midi.selected_outputs[slot] {
midi_outputs
.get(idx)
.map(|d| d.name.clone())
.unwrap_or_else(|| "(disconnected)".to_string())
} else if midi_outputs.is_empty() {
"(none found)".to_string()
} else {
"(not connected)".to_string()
}
};
let midi_in_display = |slot: usize| -> String {
if let Some(idx) = app.midi.selected_inputs[slot] {
midi_inputs
.get(idx)
.map(|d| d.name.clone())
.unwrap_or_else(|| "(disconnected)".to_string())
} else if midi_inputs.is_empty() {
"(none found)".to_string()
} else {
"(not connected)".to_string()
}
};
let midi_out_0 = midi_out_display(0);
let midi_out_1 = midi_out_display(1);
let midi_out_2 = midi_out_display(2);
let midi_out_3 = midi_out_display(3);
let midi_in_0 = midi_in_display(0);
let midi_in_1 = midi_in_display(1);
let midi_in_2 = midi_in_display(2);
let midi_in_3 = midi_in_display(3);
lines.push(Line::from(""));
lines.extend([
render_section_header("MIDI OUTPUTS", &theme),
render_divider(content_width, &theme),
render_option_line("Output 0", &midi_out_0, focus == OptionsFocus::MidiOutput0, &theme),
render_option_line("Output 1", &midi_out_1, focus == OptionsFocus::MidiOutput1, &theme),
render_option_line("Output 2", &midi_out_2, focus == OptionsFocus::MidiOutput2, &theme),
render_option_line("Output 3", &midi_out_3, focus == OptionsFocus::MidiOutput3, &theme),
Line::from(""),
render_section_header("MIDI INPUTS", &theme),
render_divider(content_width, &theme),
render_option_line("Input 0", &midi_in_0, focus == OptionsFocus::MidiInput0, &theme),
render_option_line("Input 1", &midi_in_1, focus == OptionsFocus::MidiInput1, &theme),
render_option_line("Input 2", &midi_in_2, focus == OptionsFocus::MidiInput2, &theme),
render_option_line("Input 3", &midi_in_3, focus == OptionsFocus::MidiInput3, &theme),
]);
}
lines.push(Line::from("")); lines.push(Line::from(""));
lines.extend([ lines.extend([
link_header,
render_divider(content_width, &theme),
render_option_line(
"Enabled",
if link.is_enabled() { "On" } else { "Off" },
focus == OptionsFocus::LinkEnabled,
&theme,
),
render_option_line(
"Start/Stop sync",
if link.is_start_stop_sync_enabled() {
"On"
} else {
"Off"
},
focus == OptionsFocus::StartStopSync,
&theme,
),
render_option_line("Quantum", &quantum_str, focus == OptionsFocus::Quantum, &theme),
Line::from(""),
render_section_header("SESSION", &theme),
render_divider(content_width, &theme),
render_readonly_line("Tempo", &tempo_str, tempo_style, &theme),
render_readonly_line("Beat", &beat_str, value_style, &theme),
render_readonly_line("Phase", &phase_str, value_style, &theme),
Line::from(""),
render_section_header("MIDI OUTPUTS", &theme),
render_divider(content_width, &theme),
render_option_line("Output 0", &midi_out_0, focus == OptionsFocus::MidiOutput0, &theme),
render_option_line("Output 1", &midi_out_1, focus == OptionsFocus::MidiOutput1, &theme),
render_option_line("Output 2", &midi_out_2, focus == OptionsFocus::MidiOutput2, &theme),
render_option_line("Output 3", &midi_out_3, focus == OptionsFocus::MidiOutput3, &theme),
Line::from(""),
render_section_header("MIDI INPUTS", &theme),
render_divider(content_width, &theme),
render_option_line("Input 0", &midi_in_0, focus == OptionsFocus::MidiInput0, &theme),
render_option_line("Input 1", &midi_in_1, focus == OptionsFocus::MidiInput1, &theme),
render_option_line("Input 2", &midi_in_2, focus == OptionsFocus::MidiInput2, &theme),
render_option_line("Input 3", &midi_in_3, focus == OptionsFocus::MidiInput3, &theme),
Line::from(""),
render_section_header("ONBOARDING", &theme), render_section_header("ONBOARDING", &theme),
render_divider(content_width, &theme), render_divider(content_width, &theme),
render_option_line( render_option_line(
@@ -290,7 +291,7 @@ fn render_divider(width: usize, theme: &theme::ThemeColors) -> Line<'static> {
)) ))
} }
fn render_option_line<'a>(label: &'a str, value: &'a str, focused: bool, theme: &theme::ThemeColors) -> Line<'a> { fn render_option_line(label: &str, value: &str, focused: bool, theme: &theme::ThemeColors) -> Line<'static> {
let highlight = Style::new().fg(theme.hint.key).add_modifier(Modifier::BOLD); let highlight = Style::new().fg(theme.hint.key).add_modifier(Modifier::BOLD);
let normal = Style::new().fg(theme.ui.text_primary); let normal = Style::new().fg(theme.ui.text_primary);
let label_style = Style::new().fg(theme.ui.text_muted); let label_style = Style::new().fg(theme.ui.text_muted);
@@ -309,19 +310,19 @@ fn render_option_line<'a>(label: &'a str, value: &'a str, focused: bool, theme:
let padded_label = format!("{label:<label_width$}"); let padded_label = format!("{label:<label_width$}");
Line::from(vec![ Line::from(vec![
Span::styled(prefix, prefix_style), Span::styled(prefix.to_string(), prefix_style),
Span::styled(padded_label, label_style), Span::styled(padded_label, label_style),
Span::styled(value_display, value_style), Span::styled(value_display, value_style),
]) ])
} }
fn render_readonly_line<'a>(label: &'a str, value: &'a str, value_style: Style, theme: &theme::ThemeColors) -> Line<'a> { fn render_readonly_line(label: &str, value: &str, value_style: Style, theme: &theme::ThemeColors) -> Line<'static> {
let label_style = Style::new().fg(theme.ui.text_muted); let label_style = Style::new().fg(theme.ui.text_muted);
let label_width = 20; let label_width = 20;
let padded_label = format!("{label:<label_width$}"); let padded_label = format!("{label:<label_width$}");
Line::from(vec![ Line::from(vec![
Span::raw(" "), Span::styled(" ".to_string(), Style::new()),
Span::styled(padded_label, label_style), Span::styled(padded_label, label_style),
Span::styled(format!(" {value}"), value_style), Span::styled(format!(" {value}"), value_style),
]) ])