192 lines
6.6 KiB
Rust
192 lines
6.6 KiB
Rust
//! 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"));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|