tui: make the spectrum color configurable
The bars were hardcoded to `COLOR_PRIMARY`, the same muted blue as the pane borders, so the one moving thing on screen read as chrome. `spectrum_color` in the client config now picks it, parsed by ratatui's `Color: FromStr`: a `#rrggbb` triple, a color name, or a 0-255 palette index, the latter two deferring to the terminal theme. It defaults to `COLOR_RED` — the red the queue marks the playing track with — so the two agree on what "now" looks like. The default lives twice, as `COLOR_RED` for the renderer and `COLOR_RED_HEX` for the config file, one being a `Color` and the other a string a user edits. `the_default_spectrum_color_is_the_queue_red` stops them drifting. A config file is user input, so an unparsable value warns on stderr and falls back to the red instead of failing. It resolves in `run`, before the alternate screen is entered, or the warning would be drawn over and lost. Existing config files predate the key: they keep working and take the default, since every field of the `ClapSerde` opt struct is optional. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d2f687983e
commit
430c87451b
|
|
@ -69,6 +69,10 @@ pub const COLOR_PRIMARY: Color = Color::Rgb(129, 161, 193);
|
||||||
pub const COLOR_PRIMARY_DARK: Color = Color::Rgb(59, 66, 82);
|
pub const COLOR_PRIMARY_DARK: Color = Color::Rgb(59, 66, 82);
|
||||||
pub const COLOR_SECONDARY: Color = Color::Rgb(180, 142, 173);
|
pub const COLOR_SECONDARY: Color = Color::Rgb(180, 142, 173);
|
||||||
pub const COLOR_RED: Color = Color::Rgb(191, 97, 106);
|
pub const COLOR_RED: Color = Color::Rgb(191, 97, 106);
|
||||||
|
/// [`COLOR_RED`] as a config-file string — the default `spectrum_color`,
|
||||||
|
/// so the bars match the playing queue row out of the box. The pair is
|
||||||
|
/// checked by `the_default_spectrum_color_is_the_queue_red`.
|
||||||
|
pub const COLOR_RED_HEX: &str = "#bf616a";
|
||||||
pub const COLOR_GREEN: Color = Color::Rgb(163, 190, 140);
|
pub const COLOR_GREEN: Color = Color::Rgb(163, 190, 140);
|
||||||
// const COLOR_ORANGE: Color = Color::Rgb(208, 135, 112);
|
// const COLOR_ORANGE: Color = Color::Rgb(208, 135, 112);
|
||||||
// const COLOR_BRIGHT: Color = Color::Rgb(216, 222, 233);
|
// const COLOR_BRIGHT: Color = Color::Rgb(216, 222, 233);
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ use ratatui::{
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{COLOR_PRIMARY, COLOR_SECONDARY};
|
use super::{COLOR_RED, COLOR_SECONDARY};
|
||||||
|
|
||||||
/// Vertical block glyphs by eighths, index 0 = empty, 8 = full cell.
|
/// Vertical block glyphs by eighths, index 0 = empty, 8 = full cell.
|
||||||
const BLOCKS: [char; 9] = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
|
const BLOCKS: [char; 9] = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
|
||||||
|
|
@ -22,7 +22,7 @@ const BLOCKS: [char; 9] = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇',
|
||||||
/// area: one `Line` per row, top row first. Each column maps to a bin;
|
/// area: one `Line` per row, top row first. Each column maps to a bin;
|
||||||
/// its level in `[0, 1]` fills from the bottom, using partial block
|
/// its level in `[0, 1]` fills from the bottom, using partial block
|
||||||
/// glyphs for the topmost fractional cell. Pure, so it is unit-tested.
|
/// glyphs for the topmost fractional cell. Pure, so it is unit-tested.
|
||||||
fn spectrum_lines(bins: &[f32], width: usize, height: usize) -> Vec<Line<'static>> {
|
fn spectrum_lines(bins: &[f32], width: usize, height: usize, color: Color) -> Vec<Line<'static>> {
|
||||||
(0..height)
|
(0..height)
|
||||||
.map(|row| {
|
.map(|row| {
|
||||||
// Row 0 is the top; count cells up from the bottom.
|
// Row 0 is the top; count cells up from the bottom.
|
||||||
|
|
@ -40,7 +40,7 @@ fn spectrum_lines(bins: &[f32], width: usize, height: usize) -> Vec<Line<'static
|
||||||
BLOCKS[cell]
|
BLOCKS[cell]
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
Line::from(Span::styled(cells, Style::default().fg(COLOR_PRIMARY)))
|
Line::from(Span::styled(cells, Style::default().fg(color)))
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
@ -78,6 +78,9 @@ pub struct NowPlaying {
|
||||||
spectrum: Vec<f32>,
|
spectrum: Vec<f32>,
|
||||||
/// Whether to draw the spectrum row (config `spectrum`, default on).
|
/// Whether to draw the spectrum row (config `spectrum`, default on).
|
||||||
spectrum_enabled: bool,
|
spectrum_enabled: bool,
|
||||||
|
/// Bar color (config `spectrum_color`), defaulting to the red the
|
||||||
|
/// queue marks the playing track with.
|
||||||
|
spectrum_color: Color,
|
||||||
/// Whether the server output is muted.
|
/// Whether the server output is muted.
|
||||||
muted: bool,
|
muted: bool,
|
||||||
/// The server's output level, `1.0` = 100%. This is the level playback
|
/// The server's output level, `1.0` = 100%. This is the level playback
|
||||||
|
|
@ -95,6 +98,7 @@ impl Default for NowPlaying {
|
||||||
track: None,
|
track: None,
|
||||||
spectrum: Vec::new(),
|
spectrum: Vec::new(),
|
||||||
spectrum_enabled: true,
|
spectrum_enabled: true,
|
||||||
|
spectrum_color: COLOR_RED,
|
||||||
muted: false,
|
muted: false,
|
||||||
volume: 1.0,
|
volume: 1.0,
|
||||||
}
|
}
|
||||||
|
|
@ -126,6 +130,11 @@ impl NowPlaying {
|
||||||
pub fn set_spectrum_enabled(&mut self, enabled: bool) {
|
pub fn set_spectrum_enabled(&mut self, enabled: bool) {
|
||||||
self.spectrum_enabled = enabled;
|
self.spectrum_enabled = enabled;
|
||||||
}
|
}
|
||||||
|
/// Sets the bar color (from client config). Parsing the config string
|
||||||
|
/// is the caller's job, so an unusable value never reaches here.
|
||||||
|
pub fn set_spectrum_color(&mut self, color: Color) {
|
||||||
|
self.spectrum_color = color;
|
||||||
|
}
|
||||||
/// Shows or hides the spectrum row (the `v` keybinding). The server
|
/// Shows or hides the spectrum row (the `v` keybinding). The server
|
||||||
/// keeps streaming the bars; this only gates rendering.
|
/// keeps streaming the bars; this only gates rendering.
|
||||||
pub fn toggle_spectrum(&mut self) {
|
pub fn toggle_spectrum(&mut self) {
|
||||||
|
|
@ -277,14 +286,14 @@ impl NowPlaying {
|
||||||
f.render_widget(time_p, elapsed_layout[1]);
|
f.render_widget(time_p, elapsed_layout[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The spectrum: full-height accent bars filling the region left
|
// The spectrum: full-height bars filling the region left below the
|
||||||
// below the progress. Columns stretch across the pane width
|
// progress, in the configured color. Columns stretch across the
|
||||||
// regardless of the server's bin count.
|
// pane width regardless of the server's bin count.
|
||||||
if self.spectrum_enabled && !self.spectrum.is_empty() {
|
if self.spectrum_enabled && !self.spectrum.is_empty() {
|
||||||
let area = now_playing_layout[2];
|
let area = now_playing_layout[2];
|
||||||
let (width, height) = (area.width as usize, area.height as usize);
|
let (width, height) = (area.width as usize, area.height as usize);
|
||||||
if width > 0 && height > 0 {
|
if width > 0 && height > 0 {
|
||||||
let lines = spectrum_lines(&self.spectrum, width, height);
|
let lines = spectrum_lines(&self.spectrum, width, height, self.spectrum_color);
|
||||||
f.render_widget(Paragraph::new(lines), area);
|
f.render_widget(Paragraph::new(lines), area);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -349,6 +358,7 @@ mod tests {
|
||||||
}),
|
}),
|
||||||
spectrum: Vec::new(),
|
spectrum: Vec::new(),
|
||||||
spectrum_enabled: true,
|
spectrum_enabled: true,
|
||||||
|
spectrum_color: COLOR_RED,
|
||||||
muted: false,
|
muted: false,
|
||||||
volume: 1.0,
|
volume: 1.0,
|
||||||
}
|
}
|
||||||
|
|
@ -381,7 +391,7 @@ mod tests {
|
||||||
fn spectrum_lines_fill_full_height_columns() {
|
fn spectrum_lines_fill_full_height_columns() {
|
||||||
// A full-level bin fills every row of its column with the full
|
// A full-level bin fills every row of its column with the full
|
||||||
// block; a zero bin leaves every row blank.
|
// block; a zero bin leaves every row blank.
|
||||||
let lines = spectrum_lines(&[1.0, 0.0], 2, 4);
|
let lines = spectrum_lines(&[1.0, 0.0], 2, 4, COLOR_RED);
|
||||||
assert_eq!(lines.len(), 4, "one line per row");
|
assert_eq!(lines.len(), 4, "one line per row");
|
||||||
let text: Vec<String> = lines
|
let text: Vec<String> = lines
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -395,7 +405,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn spectrum_lines_grow_from_the_bottom() {
|
fn spectrum_lines_grow_from_the_bottom() {
|
||||||
// Half level over 4 rows ≈ 16 eighths → the bottom two rows fill.
|
// Half level over 4 rows ≈ 16 eighths → the bottom two rows fill.
|
||||||
let lines = spectrum_lines(&[0.5], 1, 4);
|
let lines = spectrum_lines(&[0.5], 1, 4, COLOR_RED);
|
||||||
let col: Vec<char> = lines
|
let col: Vec<char> = lines
|
||||||
.iter()
|
.iter()
|
||||||
.map(|l| l.spans[0].content.chars().next().unwrap())
|
.map(|l| l.spans[0].content.chars().next().unwrap())
|
||||||
|
|
@ -415,6 +425,34 @@ mod tests {
|
||||||
assert!(full_rows >= 2, "expected tall spectrum bars, got: {rows:?}");
|
assert!(full_rows >= 2, "expected tall spectrum bars, got: {rows:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The foreground of the first full-block cell in the buffer — the
|
||||||
|
/// color the bars are actually drawn in.
|
||||||
|
fn first_bar_color(pane: &NowPlaying) -> Option<Color> {
|
||||||
|
let backend = TestBackend::new(60, 12);
|
||||||
|
let mut terminal = Terminal::new(backend).expect("test terminal");
|
||||||
|
terminal.draw(|f| pane.render(f, f.area())).expect("draw");
|
||||||
|
let buffer = terminal.backend().buffer().clone();
|
||||||
|
(0..buffer.area.height)
|
||||||
|
.flat_map(|y| (0..buffer.area.width).map(move |x| (x, y)))
|
||||||
|
.find(|&(x, y)| buffer[(x, y)].symbol() == "█")
|
||||||
|
.and_then(|(x, y)| buffer[(x, y)].style().fg)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_bars_default_to_the_queues_playing_red() {
|
||||||
|
let mut pane = now_playing(10_000, 60_000);
|
||||||
|
pane.update_spectrum(vec![1.0; 24]);
|
||||||
|
assert_eq!(first_bar_color(&pane), Some(super::super::COLOR_RED));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_bars_take_the_configured_color() {
|
||||||
|
let mut pane = now_playing(10_000, 60_000);
|
||||||
|
pane.update_spectrum(vec![1.0; 24]);
|
||||||
|
pane.set_spectrum_color(Color::Rgb(1, 2, 3));
|
||||||
|
assert_eq!(first_bar_color(&pane), Some(Color::Rgb(1, 2, 3)));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_spectrum_row_is_hidden_when_disabled() {
|
fn the_spectrum_row_is_hidden_when_disabled() {
|
||||||
let mut pane = now_playing(10_000, 60_000);
|
let mut pane = now_playing(10_000, 60_000);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
use std::path::{Path, PathBuf};
|
use std::{
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
use crabidy_core::{clap, clap_serde_derive, serde::Serialize, ClapSerde};
|
use crabidy_core::{clap, clap_serde_derive, serde::Serialize, ClapSerde};
|
||||||
|
use ratatui::style::Color;
|
||||||
|
|
||||||
#[derive(ClapSerde, Serialize, Debug)]
|
#[derive(ClapSerde, Serialize, Debug)]
|
||||||
#[clap(author, version, about)]
|
#[clap(author, version, about)]
|
||||||
|
|
@ -169,6 +173,33 @@ pub struct ServerConfig {
|
||||||
#[default(true)]
|
#[default(true)]
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
pub spectrum: bool,
|
pub spectrum: bool,
|
||||||
|
|
||||||
|
/// Color of the frequency-spectrum bars: a `#rrggbb` hex triple, one
|
||||||
|
/// of ratatui's names ("red", "light-blue", …), or a 0-255 index into
|
||||||
|
/// the terminal palette. Defaults to the red the queue marks the
|
||||||
|
/// playing track with. An unparsable value falls back to that
|
||||||
|
/// default with a warning — see [`spectrum_color`].
|
||||||
|
#[default(crate::app::COLOR_RED_HEX.to_string())]
|
||||||
|
#[clap(long)]
|
||||||
|
pub spectrum_color: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves [`ServerConfig::spectrum_color`] to a ratatui [`Color`].
|
||||||
|
///
|
||||||
|
/// A config file is user input: a typo must not take the client down, so an
|
||||||
|
/// unparsable value is reported on stderr and the default red is used. The
|
||||||
|
/// terminal decides what a name or palette index looks like, so this cannot
|
||||||
|
/// tell a working color from an invisible one — only a syntactic one from a
|
||||||
|
/// nonsense one.
|
||||||
|
pub fn spectrum_color(config: &Config) -> Color {
|
||||||
|
let raw = config.server.spectrum_color.trim();
|
||||||
|
if raw.is_empty() {
|
||||||
|
return crate::app::COLOR_RED;
|
||||||
|
}
|
||||||
|
Color::from_str(raw).unwrap_or_else(|_| {
|
||||||
|
eprintln!("invalid spectrum_color {raw:?}, using the default");
|
||||||
|
crate::app::COLOR_RED
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -210,6 +241,45 @@ mod tests {
|
||||||
assert!(!config.server.spectrum);
|
assert!(!config.server.spectrum);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_default_spectrum_color_is_the_queue_red() {
|
||||||
|
// The config default is a string and the renderer wants a `Color`;
|
||||||
|
// this is what keeps the two spellings of the same red together.
|
||||||
|
let config = Config::default();
|
||||||
|
assert_eq!(config.server.spectrum_color, crate::app::COLOR_RED_HEX);
|
||||||
|
assert_eq!(spectrum_color(&config), crate::app::COLOR_RED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_spectrum_color_is_read_as_hex_name_or_palette_index() {
|
||||||
|
let mut config = Config::default();
|
||||||
|
for (raw, expected) in [
|
||||||
|
("#00ff7f", Color::Rgb(0, 255, 127)),
|
||||||
|
("light-blue", Color::LightBlue),
|
||||||
|
("208", Color::Indexed(208)),
|
||||||
|
// Surrounding whitespace is a config-file slip, not a value.
|
||||||
|
(" #010203 ", Color::Rgb(1, 2, 3)),
|
||||||
|
] {
|
||||||
|
config.server.spectrum_color = raw.to_string();
|
||||||
|
assert_eq!(spectrum_color(&config), expected, "parsing {raw:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_unusable_spectrum_color_falls_back_to_the_default() {
|
||||||
|
// A config file is user input: a typo warns and keeps the client
|
||||||
|
// running rather than taking it down.
|
||||||
|
let mut config = Config::default();
|
||||||
|
for raw in ["", " ", "puce", "#12345", "300", "#"] {
|
||||||
|
config.server.spectrum_color = raw.to_string();
|
||||||
|
assert_eq!(
|
||||||
|
spectrum_color(&config),
|
||||||
|
crate::app::COLOR_RED,
|
||||||
|
"rejecting {raw:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn write_auth_round_trips_user_password_address() {
|
fn write_auth_round_trips_user_password_address() {
|
||||||
let dir = TempDir::new().expect("tempdir");
|
let dir = TempDir::new().expect("tempdir");
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,11 @@ pub async fn run(config: &'static Config) -> Result<(), Box<dyn Error>> {
|
||||||
tokio::spawn(async move { orchestrate(config, (tx, rx)).await.unwrap() });
|
tokio::spawn(async move { orchestrate(config, (tx, rx)).await.unwrap() });
|
||||||
|
|
||||||
let spectrum_enabled = config.server.spectrum;
|
let spectrum_enabled = config.server.spectrum;
|
||||||
|
// Resolved here rather than in the UI thread so a bad config string is
|
||||||
|
// reported on stderr before the alternate screen swallows it.
|
||||||
|
let spectrum_color = config::spectrum_color(config);
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
run_ui(ui_tx, ui_rx, spectrum_enabled);
|
run_ui(ui_tx, ui_rx, spectrum_enabled, spectrum_color);
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|
@ -209,7 +212,12 @@ async fn poll(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_ui(tx: Sender<MessageFromUi>, rx: Receiver<MessageToUi>, spectrum_enabled: bool) {
|
fn run_ui(
|
||||||
|
tx: Sender<MessageFromUi>,
|
||||||
|
rx: Receiver<MessageToUi>,
|
||||||
|
spectrum_enabled: bool,
|
||||||
|
spectrum_color: ratatui::style::Color,
|
||||||
|
) {
|
||||||
// setup terminal
|
// setup terminal
|
||||||
enable_raw_mode().unwrap();
|
enable_raw_mode().unwrap();
|
||||||
let mut stdout = io::stdout();
|
let mut stdout = io::stdout();
|
||||||
|
|
@ -220,6 +228,7 @@ fn run_ui(tx: Sender<MessageFromUi>, rx: Receiver<MessageToUi>, spectrum_enabled
|
||||||
// create app and run it
|
// create app and run it
|
||||||
let mut app = App::new(tx);
|
let mut app = App::new(tx);
|
||||||
app.now_playing.set_spectrum_enabled(spectrum_enabled);
|
app.now_playing.set_spectrum_enabled(spectrum_enabled);
|
||||||
|
app.now_playing.set_spectrum_color(spectrum_color);
|
||||||
let tick_rate = Duration::from_millis(100);
|
let tick_rate = Duration::from_millis(100);
|
||||||
let mut last_tick = Instant::now();
|
let mut last_tick = Instant::now();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ its visible set is recomputed.
|
||||||
## Frequency spectrum
|
## Frequency spectrum
|
||||||
|
|
||||||
A row of frequency bars is drawn under the track progress while audio
|
A row of frequency bars is drawn under the track progress while audio
|
||||||
plays, as block glyphs (`▁▂▃▄▅▆▇█`) in the accent color. The bars are
|
plays, as block glyphs (`▁▂▃▄▅▆▇█`). The bars are
|
||||||
produced on the **server**: it taps its own audio output, runs an FFT
|
produced on the **server**: it taps its own audio output, runs an FFT
|
||||||
(~20 fps), folds the result into a few log-spaced bins, and streams
|
(~20 fps), folds the result into a few log-spaced bins, and streams
|
||||||
them on the update stream like every other bit of live state. So the
|
them on the update stream like every other bit of live state. So the
|
||||||
|
|
@ -147,6 +147,15 @@ config sets the startup default (`spectrum = true` is the default;
|
||||||
`false` starts hidden). Either way it is only a display choice — the
|
`false` starts hidden). Either way it is only a display choice — the
|
||||||
server always computes and streams the bars while audio flows.
|
server always computes and streams the bars while audio flows.
|
||||||
|
|
||||||
|
`spectrum_color` in the client config picks their color, as a `#rrggbb`
|
||||||
|
hex triple, a color name (`red`, `light-blue`, …), or a 0-255 index into
|
||||||
|
the terminal palette. It defaults to the red the queue marks the playing
|
||||||
|
track with, so the two agree on what "now" looks like. A palette index or
|
||||||
|
a name leaves the shade to your terminal theme, which is the point of
|
||||||
|
them; a hex triple asks for one exact color, which a 256-color terminal
|
||||||
|
will approximate. An unparsable value warns on stderr and falls back to
|
||||||
|
the default rather than stopping the client.
|
||||||
|
|
||||||
## Key bindings
|
## Key bindings
|
||||||
|
|
||||||
Global keys work in either pane. Pane keys apply only while that pane is
|
Global keys work in either pane. Pane keys apply only while that pane is
|
||||||
|
|
|
||||||
|
|
@ -105,9 +105,16 @@ password = ""
|
||||||
|
|
||||||
# Show the frequency-spectrum bars under the track progress.
|
# Show the frequency-spectrum bars under the track progress.
|
||||||
spectrum = true
|
spectrum = true
|
||||||
|
|
||||||
|
# Color of those bars: a "#rrggbb" hex triple, a color name ("red",
|
||||||
|
# "light-blue", …), or a 0-255 index into the terminal palette. The
|
||||||
|
# default is the red the queue marks the playing track with. An
|
||||||
|
# unparsable value warns on stderr and falls back to that default.
|
||||||
|
spectrum_color = "#bf616a"
|
||||||
```
|
```
|
||||||
|
|
||||||
Every option is also a command-line flag, given before the subcommand
|
Every option except `spectrum_color` is also a command-line flag, given
|
||||||
|
before the subcommand
|
||||||
(`cbd-tui --address http://pi:50051 --user owner`, `cbd --spectrum
|
(`cbd-tui --address http://pi:50051 --user owner`, `cbd --spectrum
|
||||||
false`). A provided flag overrides the file value; an omitted flag
|
false`). A provided flag overrides the file value; an omitted flag
|
||||||
leaves the file value in place. To write credentials into the config
|
leaves the file value in place. To write credentials into the config
|
||||||
|
|
|
||||||
|
|
@ -1640,3 +1640,31 @@ a nudge next to a skip.
|
||||||
`font-variant-emoji: text` on the controls row asks for the text form of the
|
`font-variant-emoji: text` on the controls row asks for the text form of the
|
||||||
prev/next/pause marks, which have the same emoji presentation. It is ignored
|
prev/next/pause marks, which have the same emoji presentation. It is ignored
|
||||||
where unsupported, at no cost: those glyphs are the fallback anyway.
|
where unsupported, at no cost: those glyphs are the fallback anyway.
|
||||||
|
|
||||||
|
## TUI spectrum color (2026-07-27)
|
||||||
|
|
||||||
|
The bars were hardcoded to `COLOR_PRIMARY`, the same muted blue as the pane
|
||||||
|
borders, so they read as chrome rather than as the thing that is moving.
|
||||||
|
|
||||||
|
`spectrum_color` in the client config now picks the color, parsed by ratatui's
|
||||||
|
`Color: FromStr` — so a `#rrggbb` triple, a color name, or a 0-255 palette
|
||||||
|
index all work, and a name or index defers to the terminal theme. It defaults
|
||||||
|
to `COLOR_RED`, the red the queue marks the playing track with, so the two
|
||||||
|
agree on what "now" looks like.
|
||||||
|
|
||||||
|
The default lives twice, as `COLOR_RED` for the renderer and `COLOR_RED_HEX`
|
||||||
|
for the config file, since one is a `Color` and the other a string a user
|
||||||
|
edits. `the_default_spectrum_color_is_the_queue_red` is what stops them
|
||||||
|
drifting.
|
||||||
|
|
||||||
|
A config file is user input, so `config::spectrum_color` reports an unparsable
|
||||||
|
value on stderr and falls back to the red rather than failing. It resolves in
|
||||||
|
`run`, before the alternate screen is entered, or the warning would be drawn
|
||||||
|
over and lost. Empty counts as unset for the same reason a missing key does.
|
||||||
|
|
||||||
|
No CLI flag: the other options have one because they are things you override
|
||||||
|
per invocation, and a color is not.
|
||||||
|
|
||||||
|
Existing config files predate the key. They keep working — every field of the
|
||||||
|
`ClapSerde` opt struct is optional — and take the default, but the new key
|
||||||
|
only appears in files written from now on.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue