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
+43 -38
View File
@@ -1,15 +1,14 @@
use crate::models::FileDisplayData;
use crate::ui::{
BYTES_PER_MB, HIGHLIGHT_SYMBOL, border_style, border_type, format_bytes,
header_style, highlight_style,
BYTES_PER_MB, HIGHLIGHT_SYMBOL, body_style, dimmed_style, format_bytes,
header_style, highlight_style, info_style, pane_block, pane_title,
success_style, warning_style,
};
use ratatui::{
Frame,
layout::Rect,
widgets::{
Block, Borders, Cell, HighlightSpacing, Paragraph, Row, Table,
TableState,
},
text::{Line, Span},
widgets::{Cell, HighlightSpacing, Paragraph, Row, Table, TableState},
};
use std::collections::HashSet;
@@ -62,26 +61,32 @@ pub fn render_results_pane(
active_search_query,
} = params;
if items.is_empty() {
let title = if let Some(query) = active_search_query {
format!("[2] Results: {query}")
} else {
"[2] Results".to_string()
let title = match active_search_query {
Some(query) => format!("Results: {query}"),
None => "Results".to_string(),
};
let empty_block = Block::default()
.borders(Borders::ALL)
.border_style(border_style(focused))
.border_type(border_type(focused))
.title(title);
let empty_block =
pane_block(focused).title(pane_title("2", &title, focused));
let message = if is_filtering {
format!("No results match filter: '{filter_query}'")
vec![Line::from(Span::styled(
format!("No results match filter: '{filter_query}'"),
dimmed_style(),
))]
} else {
format!(
"soulseek-rs 🦀 v{VERSION}
Michel de Graaf 2026\n
No results. Select a search from the Searches pane [1]. Or start new search [s → search] \n"
)
vec![
Line::from(vec![
Span::styled("soulseek-rs", body_style()),
Span::styled(format!(" v{VERSION}"), dimmed_style()),
]),
Line::from(""),
Line::from(Span::styled("No results yet.", body_style())),
Line::from(Span::styled(
"Pick a search in [1] Searches, or start a new one.",
dimmed_style(),
)),
]
};
let paragraph = Paragraph::new(message).block(empty_block);
@@ -124,14 +129,20 @@ No results. Select a search from the Searches pane [1]. Or start new search [s
"-".to_string()
};
let checkbox_style = if checkbox == "[✓]" {
success_style()
} else {
dimmed_style()
};
Row::new(vec![
Cell::from(checkbox),
Cell::from(file.filename.clone()),
Cell::from(format_bytes(file.size)),
Cell::from(file.username.clone()),
Cell::from(bitrate_str),
Cell::from(speed_str),
Cell::from(file.slots.to_string()),
Cell::from(checkbox).style(checkbox_style),
Cell::from(file.filename.clone()).style(body_style()),
Cell::from(format_bytes(file.size)).style(warning_style()),
Cell::from(file.username.clone()).style(info_style()),
Cell::from(bitrate_str).style(dimmed_style()),
Cell::from(speed_str).style(dimmed_style()),
Cell::from(file.slots.to_string()).style(dimmed_style()),
])
})
.collect();
@@ -147,11 +158,11 @@ No results. Select a search from the Searches pane [1]. Or start new search [s
];
let title = if is_filtering {
format!("[2] Results - Filter: '{filter_query}'")
format!("Results · filter: '{filter_query}'")
} else if let Some(query) = active_search_query {
format!("[2] Results: {query}")
format!("Results: {query}")
} else {
"[2] Results".to_string()
"Results".to_string()
};
let table = Table::new(rows, widths)
@@ -159,13 +170,7 @@ No results. Select a search from the Searches pane [1]. Or start new search [s
.row_highlight_style(highlight_style())
.highlight_symbol(HIGHLIGHT_SYMBOL)
.highlight_spacing(HighlightSpacing::Always)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(border_style(focused))
.border_type(border_type(focused))
.title(title),
);
.block(pane_block(focused).title(pane_title("2", &title, focused)));
frame.render_stateful_widget(table, area, table_state);
}