Files
furumi-tui/src/ui/additional_settings.rs
T

192 lines
6.6 KiB
Rust
Raw Normal View History

2026-09-07 13:06:46 +03:00
//! Secondary settings window; child dialogs are rendered above it.
use ratatui::{
Frame,
layout::{Constraint, Layout, Rect},
text::Line,
widgets::{Block, Clear, List, ListItem, ListState, Paragraph, Wrap},
};
use super::theme;
use crate::app::state::{AppState, SettingsRow, additional_settings_rows};
pub fn draw(frame: &mut Frame, state: &AppState) {
let screen = frame.area();
let width = screen.width.saturating_sub(2).min(90);
let height = screen.height.saturating_sub(2).min(26);
let area = Rect::new(
screen.x + (screen.width - width) / 2,
screen.y + (screen.height - height) / 2,
width,
height,
);
let block = Block::bordered()
.title(" Additional settings ")
.title_style(theme::header_for(state))
.border_style(theme::strong_border_for(state));
let inner = block.inner(area);
frame.render_widget(Clear, area);
frame.render_widget(block, area);
let [list_area, status_area, footer] = Layout::vertical([
Constraint::Min(1),
Constraint::Length(3),
Constraint::Length(1),
])
.areas(inner);
let rows = additional_settings_rows(state);
let mut items = Vec::new();
let mut selected = 0;
for (index, row) in rows.iter().enumerate() {
let section = match row {
SettingsRow::MusicDirectory => Some("Library"),
SettingsRow::CheckUpdate => Some("Updates"),
SettingsRow::VisualizationClock => Some("Visualizations"),
_ => None,
};
if let Some(section) = section {
items.push(ListItem::new(Line::styled(
section,
theme::header_for(state),
)));
}
if index
== state
.additional_settings_cursor
.min(rows.len().saturating_sub(1))
{
selected = items.len();
}
let (label, value, enabled) = match row {
SettingsRow::MusicDirectory => (
"Music save directory".into(),
if state.music_dir_changing {
"checking/changing...".into()
} else {
state.music_dir.to_string_lossy().into_owned()
},
!state.music_dir_changing,
),
SettingsRow::CheckUpdate => (
"Check for updates".into(),
format!("v{}", env!("CARGO_PKG_VERSION")),
!state.updater.busy && !state.updater.installed,
),
SettingsRow::InstallUpdate => (
"Install update".into(),
state
.updater
.available
.as_ref()
.map(|u| format!("v{}", u.version))
.unwrap_or_else(|| "check for updates first".into()),
state.updater.available.is_some()
&& !state.updater.busy
&& !state.updater.installed,
),
SettingsRow::VisualizationClock => (
"Show clock".into(),
if state.visualizer.config.show_clock {
"on"
} else {
"off"
}
.into(),
true,
),
SettingsRow::VisualizationScript(index) => {
let script = &state.visualizer.scripts[*index];
let mark = if state.visualizer.selected_script_index() == Some(*index) {
"* "
} else {
""
};
(
format!("{mark}{}", script.name),
script
.path
.file_name()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_default(),
true,
)
}
SettingsRow::VisualizationNew => {
("+ New visualization script".into(), "enter".into(), true)
}
SettingsRow::VisualizationEdit => {
("Edit selected visualization".into(), "enter".into(), true)
}
_ => continue,
};
let item = ListItem::new(format!("{label}: {value}"));
items.push(if enabled {
item
} else {
item.style(theme::dim())
});
}
frame.render_stateful_widget(
List::new(items)
.highlight_symbol("> ")
.highlight_style(theme::selection_for(state)),
list_area,
&mut ListState::default().with_selected(Some(selected)),
);
let message = state
.status_message
.as_deref()
.filter(|message| !message.is_empty())
.unwrap_or(&state.updater.message);
frame.render_widget(
Paragraph::new(message)
.wrap(Wrap { trim: true })
.style(theme::dim()),
status_area,
);
frame.render_widget(
Paragraph::new("Up/Down: navigate | Enter: select | Esc: back").style(theme::dim()),
footer,
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn window_renders_controls_and_scrolls_to_last_row_in_small_terminal() {
for (width, height) in [(80, 30), (45, 14)] {
let mut terminal =
ratatui::Terminal::new(ratatui::backend::TestBackend::new(width, height)).unwrap();
let mut state = AppState::default();
state.updater.message = "No newer stable release".into();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let text: String = terminal
.backend()
.buffer()
.content
.iter()
.map(|c| c.symbol())
.collect();
assert!(text.contains("Additional settings"));
assert!(text.contains("Music save directory"));
if height >= 30 {
assert!(text.contains("Check for updates"));
assert!(text.contains("Install update"));
assert!(text.contains("Visualizations"));
}
state.additional_settings_cursor = additional_settings_rows(&state).len() - 1;
terminal.draw(|frame| draw(frame, &state)).unwrap();
let text: String = terminal
.backend()
.buffer()
.content
.iter()
.map(|c| c.symbol())
.collect();
assert!(text.contains("New visualization script"));
assert!(text.contains("No newer stable release"));
}
}
}