valentine

Terminal control panel for the Focusrite Scarlett 18i20 — a from-scratch replacement for Focusrite Control.
Log | Files | Refs | README | LICENSE

commit b8b9de94b30c3533c6af9f508834e31367e41dbc
parent 35effd89312ab0a7013f798b7f5b0a1d3ea82932
Author: Matthew Gantenbein <ganten1998@gmail.com>
Date:   Sun, 31 May 2026 18:18:39 -0500

feat: Implement Inputs panel rewrite to support all input types with per-channel strips and meters.

Co-authored-by: aider (ollama/qwen2.5-coder:14b) <aider@aider.chat>

Diffstat:
Mvalentine/src/panels/inputs.rs | 59++++++++++++++++++++++++++++-------------------------------
1 file changed, 28 insertions(+), 31 deletions(-)

diff --git a/valentine/src/panels/inputs.rs b/valentine/src/panels/inputs.rs @@ -1,5 +1,5 @@ //! The Inputs panel — a navigable grid of the per-channel preamp switches -//! (Inst/Line, Air, Pad, 48V phantom) for the 8 analogue inputs. Amber = engaged. +//! (Inst/Line, Air, Pad, 48V phantom) for all input types. Amber = engaged. //! //! Arrow keys move the cursor; Space/Enter toggles the focused switch via the //! `scarlett-core` control layer. 48V is per phantom *group* (inputs 1–4, 5–8), @@ -9,7 +9,8 @@ use ratatui::prelude::*; use ratatui::widgets::{Block, Borders, Paragraph}; use scarlett_core::controls::{InputState, InputSwitch}; -use scarlett_core::model::S18I20_GEN3; +use scarlett_core::model::{DeviceInfo, S18I20_GEN3}; +use scarlett_core::sources::{Source, SourceType}; use crate::theme::Theme; @@ -35,11 +36,11 @@ impl Col { } /// Whether this switch exists for the given 0-based input. - fn applies_to(self, input: u8) -> bool { + fn applies_to(self, source: &Source) -> bool { match self { - Col::Inst => input < S18I20_GEN3.level_input_count, - Col::Air => input < S18I20_GEN3.air_input_count, - Col::Pad => input < S18I20_GEN3.pad_input_count, + Col::Inst => source.preamp_capabilities.inst, + Col::Air => source.preamp_capabilities.air, + Col::Pad => source.preamp_capabilities.pad, Col::P48 => true, } } @@ -48,23 +49,23 @@ impl Col { /// Cursor position within the grid. #[derive(Debug, Clone, Copy)] pub struct Cursor { - pub input: u8, + pub source_index: usize, pub col: usize, } impl Default for Cursor { fn default() -> Self { - Cursor { input: 0, col: 1 } // start on AIR of input 1 + Cursor { source_index: 0, col: 1 } // start on AIR of the first source } } impl Cursor { pub fn up(&mut self) { - self.input = self.input.saturating_sub(1); + self.source_index = self.source_index.saturating_sub(1); } - pub fn down(&mut self) { - if self.input + 1 < S18I20_GEN3.air_input_count { - self.input += 1; + pub fn down(&mut self, sources: &[Source]) { + if self.source_index + 1 < sources.len() { + self.source_index += 1; } } pub fn left(&mut self) { @@ -81,15 +82,14 @@ impl Cursor { } } -/// Is the switch at (input, col) currently on, per `state`? -fn is_on(state: &InputState, input: u8, col: Col) -> bool { - let i = input as usize; +/// Is the switch at (source, col) currently on, per `state`? +fn is_on(state: &InputState, source: &Source, col: Col) -> bool { match col { - Col::Inst => state.inst.get(i).copied().unwrap_or(false), - Col::Air => state.air.get(i).copied().unwrap_or(false), - Col::Pad => state.pad.get(i).copied().unwrap_or(false), + Col::Inst => state.inst.get(source.index as usize).copied().unwrap_or(false), + Col::Air => state.air.get(source.index as usize).copied().unwrap_or(false), + Col::Pad => state.pad.get(source.index as usize).copied().unwrap_or(false), Col::P48 => { - let group = (input / S18I20_GEN3.inputs_per_phantom) as usize; + let group = (source.index / S18I20_GEN3.inputs_per_phantom) as usize; state.phantom.get(group).copied().unwrap_or(false) } } @@ -111,6 +111,7 @@ pub fn render( area: Rect, theme: &Theme, state: &InputState, + sources: &[Source], cursor: Cursor, focused: bool, ) { @@ -127,7 +128,7 @@ pub fn render( // Header row. let mut header = vec![Span::styled( - format!("{:<10}", "Input"), + format!("{:<10}", "Source"), Style::default().fg(theme.fg_dim), )]; for col in Col::ALL { @@ -139,13 +140,9 @@ pub fn render( lines.push(Line::from(header)); lines.push(Line::from("")); - // One row per analogue input. - for input in 0..S18I20_GEN3.air_input_count { - let label = if input < S18I20_GEN3.level_input_count { - format!("In {} (HiZ)", input + 1) - } else { - format!("In {}", input + 1) - }; + // One row per source. + for (si, source) in sources.iter().enumerate() { + let label = format!("{} {}", source.name, if source.stereo_pair { "(Stereo)" } else { "" }); let mut row = vec![Span::styled( format!("{label:<10}"), Style::default().fg(theme.fg), @@ -153,12 +150,12 @@ pub fn render( for (ci, col) in Col::ALL.iter().enumerate() { let col = *col; - let here = focused && cursor.input == input && cursor.col == ci; + let here = focused && cursor.source_index == si && cursor.col == ci; - let cell = if !col.applies_to(input) { + let cell = if !col.applies_to(source) { Span::styled(format!("{:^7}", "·"), Style::default().fg(theme.fg_dim)) } else { - let on = is_on(state, input, col); + let on = is_on(state, source, col); let glyph = if on { "● ON" } else { " ·" }; let mut style = if on { Style::default().fg(theme.armed).add_modifier(Modifier::BOLD) @@ -181,7 +178,7 @@ pub fn render( lines.push(Line::from("")); lines.push(Line::from(Span::styled( - "↑↓ input ←→ switch space/enter toggle", + "↑↓ source ←→ switch space/enter toggle", Style::default().fg(theme.fg_dim), )));