feat(ui): theme the TUI with the soulseek-rs design system

Replace the ANSI Light* palette with the design system's tokens
(tokens/colors.css): oxide accent, phosphor focus, tape/signal/alarm
semantics, paper text on dust chrome. Only foregrounds and the row
highlight are painted so the user's terminal background shows through.

Route every bordered pane through a single pane_block() helper carrying
one column of horizontal padding, and give legends a consistent
[n] Title form with the tag in signal. The command bar now derives its
cursor position from the block's inner rect instead of a hardcoded
border offset.

Also collapses the download-status glyph match that was duplicated
across three render sites into download_status_glyph().
This commit is contained in:
Michel de Graaf
2026-07-24 21:18:20 +02:00
parent 2c29e1a39b
commit 105c798855
11 changed files with 362 additions and 365 deletions
+21 -14
View File
@@ -5,14 +5,18 @@
//! machine so it can be tested without a terminal; the IO loop
//! ([`run_login_flow`]) drives it against a real terminal and client.
use crate::ui::{
GLYPH_CURSOR, accent_style, dimmed_style, error_style, info_style,
pane_block, primary_style, title_style, warning_style,
};
use color_eyre::Result;
use ratatui::{
DefaultTerminal, Frame,
crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, poll},
layout::{Constraint, Flex, Layout, Rect},
style::{Color, Modifier, Style},
style::Modifier,
text::{Line, Span},
widgets::{Block, Borders, Clear, Paragraph},
widgets::{Clear, Paragraph},
};
use soulseek_rs::{Client, ClientSettings};
use std::sync::mpsc::{Receiver, channel};
@@ -231,10 +235,10 @@ fn render(frame: &mut Frame, form: &LoginForm) {
let area = centered(frame.area(), 52, 12);
frame.render_widget(Clear, area);
let block = Block::default()
.borders(Borders::ALL)
.title(" Soulseek Login ")
.title_style(Style::default().add_modifier(Modifier::BOLD));
let block = pane_block(true).title(Line::from(vec![
Span::styled(" soulseek-rs", primary_style()),
Span::styled(" login ", title_style(true)),
]));
let inner = block.inner(area);
frame.render_widget(block, area);
@@ -249,16 +253,19 @@ fn render(frame: &mut Frame, form: &LoginForm) {
.split(inner);
let field = |label: &str, value: String, focused: bool| {
let marker = if focused { "> " } else { " " };
let marker = if focused { "› " } else { " " };
let style = if focused {
Style::default().add_modifier(Modifier::BOLD)
info_style().add_modifier(Modifier::BOLD)
} else {
Style::default()
dimmed_style()
};
Paragraph::new(Line::from(vec![
Span::styled(format!("{marker}{label:<10}"), style),
Span::raw(value),
Span::raw(if focused { "▏" } else { "" }),
Span::styled(value, primary_style()),
Span::styled(
if focused { GLYPH_CURSOR } else { "" },
accent_style(),
),
]))
};
@@ -284,10 +291,10 @@ fn render(frame: &mut Frame, form: &LoginForm) {
LoginPhase::Editing => Paragraph::new(""),
LoginPhase::Connecting => Paragraph::new(Line::from(Span::styled(
"Connecting…",
Style::default().fg(Color::Yellow),
warning_style(),
))),
LoginPhase::Failed(message) => Paragraph::new(Line::from(
Span::styled(message.clone(), Style::default().fg(Color::Red)),
Span::styled(message.clone(), error_style()),
))
.wrap(ratatui::widgets::Wrap { trim: true }),
};
@@ -298,7 +305,7 @@ fn render(frame: &mut Frame, form: &LoginForm) {
"New usernames are registered automatically.\n\
Tab: switch · Enter: log in · Esc: quit",
)
.style(Style::default().fg(Color::DarkGray)),
.style(dimmed_style()),
rows[5],
);
}