More robust midi implementation

This commit is contained in:
2026-01-31 23:58:57 +01:00
parent dfd024cab7
commit 96e7fb6bc4
12 changed files with 393 additions and 201 deletions

View File

@@ -31,7 +31,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;
// Build link header with status
let enabled = link.is_enabled();
let peers = link.peers();
let (status_text, status_color) = if !enabled {
@@ -64,7 +63,6 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
Span::styled(peer_text, Style::new().fg(theme.ui.text_muted)),
]);
// Prepare values
let flash_str = format!("{:.0}%", app.ui.flash_brightness * 100.0);
let quantum_str = format!("{:.0}", link.quantum());
let tempo_str = format!("{:.1} BPM", link.tempo());
@@ -74,35 +72,45 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
let tempo_style = Style::new().fg(theme.values.tempo).add_modifier(Modifier::BOLD);
let value_style = Style::new().fg(theme.values.value);
// MIDI device lists
let midi_outputs = midi::list_midi_outputs();
let midi_inputs = midi::list_midi_inputs();
let midi_out_display = if let Some(idx) = app.midi.selected_output {
midi_outputs
.get(idx)
.map(|d| d.name.as_str())
.unwrap_or("(disconnected)")
} else if midi_outputs.is_empty() {
"(none found)"
} else {
"(not connected)"
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 = if let Some(idx) = app.midi.selected_input {
midi_inputs
.get(idx)
.map(|d| d.name.as_str())
.unwrap_or("(disconnected)")
} else if midi_inputs.is_empty() {
"(none found)"
} else {
"(not connected)"
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()
}
};
// Build flat list of all lines
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 lines: Vec<Line> = vec![
// DISPLAY section (lines 0-8)
render_section_header("DISPLAY", &theme),
render_divider(content_width, &theme),
render_option_line(
@@ -146,9 +154,7 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
&theme,
),
render_option_line("Flash brightness", &flash_str, focus == OptionsFocus::FlashBrightness, &theme),
// Blank line (line 9)
Line::from(""),
// ABLETON LINK section (lines 10-15)
link_header,
render_divider(content_width, &theme),
render_option_line(
@@ -168,27 +174,31 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
&theme,
),
render_option_line("Quantum", &quantum_str, focus == OptionsFocus::Quantum, &theme),
// Blank line (line 16)
Line::from(""),
// SESSION section (lines 17-20)
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),
// Blank line (line 22)
Line::from(""),
// MIDI section (lines 23-26)
render_section_header("MIDI", &theme),
render_section_header("MIDI OUTPUTS", &theme),
render_divider(content_width, &theme),
render_option_line("Output", midi_out_display, focus == OptionsFocus::MidiOutput, &theme),
render_option_line("Input", midi_in_display, focus == OptionsFocus::MidiInput, &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),
];
let total_lines = lines.len();
let max_visible = padded.height as usize;
// Map focus to line index
let focus_line: usize = match focus {
OptionsFocus::ColorScheme => 2,
OptionsFocus::RefreshRate => 3,
@@ -200,11 +210,16 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
OptionsFocus::LinkEnabled => 12,
OptionsFocus::StartStopSync => 13,
OptionsFocus::Quantum => 14,
OptionsFocus::MidiOutput => 25,
OptionsFocus::MidiInput => 26,
OptionsFocus::MidiOutput0 => 25,
OptionsFocus::MidiOutput1 => 26,
OptionsFocus::MidiOutput2 => 27,
OptionsFocus::MidiOutput3 => 28,
OptionsFocus::MidiInput0 => 32,
OptionsFocus::MidiInput1 => 33,
OptionsFocus::MidiInput2 => 34,
OptionsFocus::MidiInput3 => 35,
};
// Calculate scroll offset to keep focused line visible (centered when possible)
let scroll_offset = if total_lines <= max_visible {
0
} else {
@@ -213,7 +228,6 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
.min(total_lines.saturating_sub(max_visible))
};
// Render visible portion
let visible_end = (scroll_offset + max_visible).min(total_lines);
let visible_lines: Vec<Line> = lines
.into_iter()
@@ -223,7 +237,6 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
frame.render_widget(Paragraph::new(visible_lines), padded);
// Render scroll indicators
let indicator_style = Style::new().fg(theme.ui.text_dim);
let indicator_x = padded.x + padded.width.saturating_sub(1);