This commit is contained in:
@@ -214,6 +214,11 @@ pub fn create_editor(
|
||||
let shared = editor.bridge.shared_state.load();
|
||||
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
|
||||
editor.app.metrics.scope = editor.bridge.scope_buffer.read();
|
||||
(editor.app.metrics.peak_left, editor.app.metrics.peak_right) =
|
||||
|
||||
@@ -91,6 +91,7 @@ impl App {
|
||||
Self::build(variables, dict, rng, false)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn new_plugin(variables: Variables, dict: Dictionary, rng: Rng) -> Self {
|
||||
Self::build(variables, dict, rng, true)
|
||||
}
|
||||
|
||||
@@ -158,12 +158,17 @@ pub struct SharedSequencerState {
|
||||
pub active_patterns: Vec<ActivePatternState>,
|
||||
pub step_traces: Arc<StepTracesMap>,
|
||||
pub event_count: usize,
|
||||
pub tempo: f64,
|
||||
pub beat: f64,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct SequencerSnapshot {
|
||||
pub active_patterns: Vec<ActivePatternState>,
|
||||
step_traces: Arc<StepTracesMap>,
|
||||
pub event_count: usize,
|
||||
pub tempo: f64,
|
||||
pub beat: f64,
|
||||
}
|
||||
|
||||
impl From<&SharedSequencerState> for SequencerSnapshot {
|
||||
@@ -172,6 +177,8 @@ impl From<&SharedSequencerState> for SequencerSnapshot {
|
||||
active_patterns: s.active_patterns.clone(),
|
||||
step_traces: Arc::clone(&s.step_traces),
|
||||
event_count: s.event_count,
|
||||
tempo: s.tempo,
|
||||
beat: s.beat,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,6 +190,8 @@ impl SequencerSnapshot {
|
||||
active_patterns: Vec::new(),
|
||||
step_traces: Arc::new(HashMap::new()),
|
||||
event_count: 0,
|
||||
tempo: 0.0,
|
||||
beat: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,11 +231,7 @@ pub struct SequencerHandle {
|
||||
impl SequencerHandle {
|
||||
pub fn snapshot(&self) -> SequencerSnapshot {
|
||||
let state = self.shared_state.load();
|
||||
SequencerSnapshot {
|
||||
active_patterns: state.active_patterns.clone(),
|
||||
step_traces: Arc::clone(&state.step_traces),
|
||||
event_count: state.event_count,
|
||||
}
|
||||
SequencerSnapshot::from(state.as_ref())
|
||||
}
|
||||
|
||||
pub fn swap_audio_channel(&self) -> Receiver<AudioCommand> {
|
||||
@@ -563,6 +568,8 @@ pub struct SequencerState {
|
||||
cc_access: Option<Arc<dyn CcAccess>>,
|
||||
muted: std::collections::HashSet<(usize, usize)>,
|
||||
soloed: std::collections::HashSet<(usize, usize)>,
|
||||
last_tempo: f64,
|
||||
last_beat: f64,
|
||||
}
|
||||
|
||||
impl SequencerState {
|
||||
@@ -592,6 +599,8 @@ impl SequencerState {
|
||||
cc_access,
|
||||
muted: 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 {
|
||||
self.process_commands(input.commands);
|
||||
self.last_tempo = input.tempo;
|
||||
self.last_beat = input.beat;
|
||||
|
||||
if !input.playing {
|
||||
return self.tick_paused();
|
||||
@@ -1092,6 +1103,8 @@ impl SequencerState {
|
||||
.collect(),
|
||||
step_traces: Arc::clone(&self.step_traces),
|
||||
event_count: self.event_count,
|
||||
tempo: self.last_tempo,
|
||||
beat: self.last_beat,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
KeyCode::Char('?') => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
|
||||
}
|
||||
KeyCode::Char(' ') => {
|
||||
KeyCode::Char(' ') if !ctx.app.plugin_mode => {
|
||||
ctx.dispatch(AppCommand::TogglePlaying);
|
||||
ctx.playing
|
||||
.store(ctx.app.playback.playing, Ordering::Relaxed);
|
||||
|
||||
@@ -77,7 +77,7 @@ pub(super) fn handle_help_page(ctx: &mut InputContext, key: KeyEvent) -> InputRe
|
||||
KeyCode::Char('?') => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
|
||||
}
|
||||
KeyCode::Char(' ') => {
|
||||
KeyCode::Char(' ') if !ctx.app.plugin_mode => {
|
||||
ctx.dispatch(AppCommand::TogglePlaying);
|
||||
ctx.playing
|
||||
.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('?') => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
|
||||
}
|
||||
KeyCode::Char(' ') => {
|
||||
KeyCode::Char(' ') if !ctx.app.plugin_mode => {
|
||||
ctx.dispatch(AppCommand::TogglePlaying);
|
||||
ctx.playing
|
||||
.store(ctx.app.playback.playing, Ordering::Relaxed);
|
||||
|
||||
@@ -29,7 +29,7 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool
|
||||
selected: false,
|
||||
}));
|
||||
}
|
||||
KeyCode::Char(' ') => {
|
||||
KeyCode::Char(' ') if !ctx.app.plugin_mode => {
|
||||
ctx.dispatch(AppCommand::TogglePlaying);
|
||||
ctx.playing
|
||||
.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('l') => super::open_load(ctx),
|
||||
KeyCode::Char('+') | KeyCode::Char('=') => ctx.dispatch(AppCommand::TempoUp),
|
||||
KeyCode::Char('-') => ctx.dispatch(AppCommand::TempoDown),
|
||||
KeyCode::Char('T') => {
|
||||
KeyCode::Char('+') | KeyCode::Char('=') if !ctx.app.plugin_mode => {
|
||||
ctx.dispatch(AppCommand::TempoUp);
|
||||
}
|
||||
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());
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::SetTempo(current)));
|
||||
}
|
||||
|
||||
@@ -677,7 +677,7 @@ fn handle_options_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect)
|
||||
let focus = ctx.app.options.focus;
|
||||
let plugin_mode = ctx.app.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 scroll_offset = if total_lines <= max_visible {
|
||||
|
||||
@@ -210,7 +210,7 @@ pub(super) fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> Inpu
|
||||
KeyCode::Left | KeyCode::Right => {
|
||||
cycle_option_value(ctx, key.code == KeyCode::Right);
|
||||
}
|
||||
KeyCode::Char(' ') => {
|
||||
KeyCode::Char(' ') if !ctx.app.plugin_mode => {
|
||||
ctx.dispatch(AppCommand::TogglePlaying);
|
||||
ctx.playing
|
||||
.store(ctx.app.playback.playing, Ordering::Relaxed);
|
||||
|
||||
@@ -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.playing
|
||||
.store(ctx.app.playback.playing, Ordering::Relaxed);
|
||||
|
||||
@@ -56,9 +56,31 @@ impl CyclicEnum for OptionsFocus {
|
||||
];
|
||||
}
|
||||
|
||||
// Line indices when Font/ZoomFactor are shown (plugin mode).
|
||||
// In terminal mode, Font/ZoomFactor are absent; all lines after ShowPreview shift up by 2.
|
||||
const FOCUS_LINES: &[(OptionsFocus, usize)] = &[
|
||||
const PLUGIN_ONLY: &[OptionsFocus] = &[
|
||||
OptionsFocus::Font,
|
||||
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::HueRotation, 3),
|
||||
(OptionsFocus::RefreshRate, 4),
|
||||
@@ -70,49 +92,110 @@ const FOCUS_LINES: &[(OptionsFocus, usize)] = &[
|
||||
(OptionsFocus::Font, 10),
|
||||
(OptionsFocus::ZoomFactor, 11),
|
||||
(OptionsFocus::WindowSize, 12),
|
||||
// blank=13, ABLETON LINK header=14, divider=15
|
||||
(OptionsFocus::LinkEnabled, 16),
|
||||
(OptionsFocus::StartStopSync, 17),
|
||||
(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::MidiOutput1, 29),
|
||||
(OptionsFocus::MidiOutput2, 30),
|
||||
(OptionsFocus::MidiOutput3, 31),
|
||||
// blank=32, MIDI INPUTS header=33, divider=34
|
||||
(OptionsFocus::MidiInput0, 35),
|
||||
(OptionsFocus::MidiInput1, 36),
|
||||
(OptionsFocus::MidiInput2, 37),
|
||||
(OptionsFocus::MidiInput3, 38),
|
||||
// blank=39, ONBOARDING header=40, divider=41
|
||||
(OptionsFocus::ResetOnboarding, 42),
|
||||
];
|
||||
|
||||
const PLUGIN_ONLY: &[OptionsFocus] = &[OptionsFocus::Font, OptionsFocus::ZoomFactor, OptionsFocus::WindowSize];
|
||||
|
||||
impl OptionsFocus {
|
||||
fn is_plugin_only(self) -> bool {
|
||||
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 {
|
||||
let base = FOCUS_LINES
|
||||
visible_layout(plugin_mode)
|
||||
.iter()
|
||||
.find(|(f, _)| *f == self)
|
||||
.map(|(_, l)| *l)
|
||||
.unwrap_or(0);
|
||||
if plugin_mode || base <= 9 {
|
||||
base
|
||||
} else {
|
||||
base - 3
|
||||
}
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
pub fn at_line(line: usize, plugin_mode: bool) -> Option<OptionsFocus> {
|
||||
FOCUS_LINES.iter().find_map(|(f, l)| {
|
||||
if f.is_plugin_only() && !plugin_mode {
|
||||
return None;
|
||||
visible_layout(plugin_mode)
|
||||
.iter()
|
||||
.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)]
|
||||
@@ -125,7 +208,7 @@ impl OptionsState {
|
||||
let mut f = self.focus;
|
||||
loop {
|
||||
f = f.next();
|
||||
if !f.is_plugin_only() || plugin_mode {
|
||||
if f.is_visible(plugin_mode) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -136,7 +219,7 @@ impl OptionsState {
|
||||
let mut f = self.focus;
|
||||
loop {
|
||||
f = f.prev();
|
||||
if !f.is_plugin_only() || plugin_mode {
|
||||
if f.is_visible(plugin_mode) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ pub fn bindings_for(page: Page, plugin_mode: bool) -> Vec<(&'static str, &'stati
|
||||
// Page-specific bindings
|
||||
match page {
|
||||
Page::Main => {
|
||||
if !plugin_mode {
|
||||
bindings.push(("Space", "Play/Stop", "Toggle playback"));
|
||||
}
|
||||
bindings.push(("←→↑↓", "Navigate", "Move cursor between steps"));
|
||||
bindings.push(("Shift+←→↑↓", "Select", "Extend 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(("< >", "Length", "Decrease/increase pattern length"));
|
||||
bindings.push(("[ ]", "Speed", "Decrease/increase pattern speed"));
|
||||
if !plugin_mode {
|
||||
bindings.push(("+ -", "Tempo", "Increase/decrease tempo"));
|
||||
bindings.push(("T", "Set tempo", "Open tempo input"));
|
||||
}
|
||||
bindings.push(("L", "Set length", "Open length input"));
|
||||
bindings.push(("S", "Set speed", "Open speed input"));
|
||||
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 => {
|
||||
bindings.push(("←→↑↓", "Navigate", "Move between banks/patterns"));
|
||||
bindings.push(("Enter", "Select", "Select pattern for editing"));
|
||||
if !plugin_mode {
|
||||
bindings.push(("Space", "Play", "Toggle pattern playback"));
|
||||
}
|
||||
bindings.push(("Esc", "Back", "Clear staged or go back"));
|
||||
bindings.push(("c", "Commit", "Commit staged changes"));
|
||||
bindings.push(("r", "Rename", "Rename bank/pattern"));
|
||||
@@ -72,21 +78,27 @@ pub fn bindings_for(page: Page, plugin_mode: bool) -> Vec<(&'static str, &'stati
|
||||
bindings.push(("↑↓", "Navigate", "Navigate list items"));
|
||||
bindings.push(("PgUp/Dn", "Page", "Page through device list"));
|
||||
bindings.push(("Enter", "Select", "Select device"));
|
||||
if !plugin_mode {
|
||||
bindings.push(("R", "Restart", "Restart audio engine"));
|
||||
}
|
||||
bindings.push(("A", "Add path", "Add sample path"));
|
||||
bindings.push(("D", "Refresh/Del", "Refresh devices or delete path"));
|
||||
bindings.push(("h", "Hush", "Stop all sounds gracefully"));
|
||||
bindings.push(("p", "Panic", "Stop all sounds immediately"));
|
||||
bindings.push(("r", "Reset", "Reset peak voice counter"));
|
||||
if !plugin_mode {
|
||||
bindings.push(("t", "Test", "Play test tone"));
|
||||
}
|
||||
}
|
||||
Page::Options => {
|
||||
bindings.push(("Tab", "Next", "Move to next option"));
|
||||
bindings.push(("Shift+Tab", "Previous", "Move to previous option"));
|
||||
bindings.push(("↑↓", "Navigate", "Navigate options"));
|
||||
bindings.push(("←→", "Toggle", "Toggle or adjust option"));
|
||||
if !plugin_mode {
|
||||
bindings.push(("Space", "Play/Stop", "Toggle playback"));
|
||||
}
|
||||
}
|
||||
Page::Help => {
|
||||
bindings.push(("↑↓ j/k", "Scroll", "Scroll content"));
|
||||
bindings.push(("Tab", "Topic", "Next topic"));
|
||||
|
||||
@@ -32,84 +32,6 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
||||
let focus = app.options.focus;
|
||||
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 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,
|
||||
),
|
||||
];
|
||||
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);
|
||||
if app.plugin_mode {
|
||||
lines.push(render_option_line(
|
||||
"Font",
|
||||
&app.ui.font,
|
||||
@@ -191,6 +113,44 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
||||
&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,
|
||||
@@ -218,7 +178,45 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
||||
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(""),
|
||||
]);
|
||||
|
||||
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),
|
||||
@@ -232,7 +230,10 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
||||
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(""),
|
||||
]);
|
||||
}
|
||||
lines.push(Line::from(""));
|
||||
lines.extend([
|
||||
render_section_header("ONBOARDING", &theme),
|
||||
render_divider(content_width, &theme),
|
||||
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 normal = Style::new().fg(theme.ui.text_primary);
|
||||
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$}");
|
||||
|
||||
Line::from(vec![
|
||||
Span::styled(prefix, prefix_style),
|
||||
Span::styled(prefix.to_string(), prefix_style),
|
||||
Span::styled(padded_label, label_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_width = 20;
|
||||
let padded_label = format!("{label:<label_width$}");
|
||||
|
||||
Line::from(vec![
|
||||
Span::raw(" "),
|
||||
Span::styled(" ".to_string(), Style::new()),
|
||||
Span::styled(padded_label, label_style),
|
||||
Span::styled(format!(" {value}"), value_style),
|
||||
])
|
||||
|
||||
Reference in New Issue
Block a user