Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add Custom preset to handle user edits to presets and persist them, c…
…hange config to track presets
  • Loading branch information
Moaht committed Apr 29, 2026
commit 3f661c55824fdc0e6d4bc02cee86e878909009c5
101 changes: 46 additions & 55 deletions src/core/app.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::core::sort::{SortContext, SortState};
use crate::core::user_config::UserConfig;
use crate::core::user_config::{ UserConfig, color_to_string };
use crate::infra::network::sync::{PartySession, PartyStatus};
use crate::infra::network::IoEvent;
use crate::tui::event::Key;
Expand Down Expand Up @@ -3224,36 +3224,12 @@ impl App {
},
],
SettingsCategory::Theme => {
fn color_to_string(color: ratatui::style::Color) -> String {
match color {
ratatui::style::Color::Rgb(r, g, b) => format!("{},{},{}", r, g, b),
ratatui::style::Color::Reset => "Reset".to_string(),
ratatui::style::Color::Black => "Black".to_string(),
ratatui::style::Color::Red => "Red".to_string(),
ratatui::style::Color::Green => "Green".to_string(),
ratatui::style::Color::Yellow => "Yellow".to_string(),
ratatui::style::Color::Blue => "Blue".to_string(),
ratatui::style::Color::Magenta => "Magenta".to_string(),
ratatui::style::Color::Cyan => "Cyan".to_string(),
ratatui::style::Color::Gray => "Gray".to_string(),
ratatui::style::Color::DarkGray => "DarkGray".to_string(),
ratatui::style::Color::LightRed => "LightRed".to_string(),
ratatui::style::Color::LightGreen => "LightGreen".to_string(),
ratatui::style::Color::LightYellow => "LightYellow".to_string(),
ratatui::style::Color::LightBlue => "LightBlue".to_string(),
ratatui::style::Color::LightMagenta => "LightMagenta".to_string(),
ratatui::style::Color::LightCyan => "LightCyan".to_string(),
ratatui::style::Color::White => "White".to_string(),
_ => "Unknown".to_string(),
}
}

vec![
SettingItem {
id: "theme.preset".to_string(),
name: "Theme Preset".to_string(),
description: "Choose a preset theme or customize below".to_string(),
value: SettingValue::Preset("Default (Cyan)".to_string()), // Default preset
value: SettingValue::Preset(self.user_config.current_preset.name().to_string()),
},
SettingItem {
id: "theme.active".to_string(),
Expand Down Expand Up @@ -3330,8 +3306,10 @@ impl App {
self.settings_unsaved_prompt_save_selected = true;
}

/// Apply changes from settings_items back to user_config
// Apply changes from settings_items back to user_config
pub fn apply_settings_changes(&mut self) {
use crate::core::user_config::{parse_theme_item, ThemePreset};

for setting in &self.settings_items {
match setting.id.as_str() {
// Behavior settings
Expand Down Expand Up @@ -3653,92 +3631,105 @@ impl App {
}
}
}
// Theme preset - applies all colors at once
// Decides whether the per-color changes following will apply.
// A named preset takes priority; the user's custom_theme is preserved
// so they can return to it later by selecting Custom.
"theme.preset" => {
if let SettingValue::Preset(preset_name) = &setting.value {
use crate::core::user_config::ThemePreset;
let preset = ThemePreset::from_name(preset_name);
if let SettingValue::Preset(name) = &setting.value {
let preset = ThemePreset::from_name(name);
self.user_config.current_preset = preset;
if preset != ThemePreset::Custom {
// Apply the preset's theme colors
self.user_config.theme = preset.to_theme();
}
}
}
// Individual theme color overrides
"theme.active" => {
// Individual theme color overrides only apply when on Custom; they
// update both the active theme and the persisted custom_theme.
"theme.active" if self.user_config.current_preset == ThemePreset::Custom => {
if let SettingValue::Color(v) = &setting.value {
if let Ok(c) = crate::core::user_config::parse_theme_item(v) {
if let Ok(c) = parse_theme_item(v) {
self.user_config.theme.active = c;
self.user_config.custom_theme.active = c;
}
}
}
"theme.banner" => {
"theme.banner" if self.user_config.current_preset == ThemePreset::Custom => {
if let SettingValue::Color(v) = &setting.value {
if let Ok(c) = crate::core::user_config::parse_theme_item(v) {
if let Ok(c) = parse_theme_item(v) {
self.user_config.theme.banner = c;
self.user_config.custom_theme.banner = c;
}
}
}
"theme.hint" => {
"theme.hint" if self.user_config.current_preset == ThemePreset::Custom => {
if let SettingValue::Color(v) = &setting.value {
if let Ok(c) = crate::core::user_config::parse_theme_item(v) {
if let Ok(c) = parse_theme_item(v) {
self.user_config.theme.hint = c;
self.user_config.custom_theme.hint = c;
}
}
}
"theme.hovered" => {
"theme.hovered" if self.user_config.current_preset == ThemePreset::Custom => {
if let SettingValue::Color(v) = &setting.value {
if let Ok(c) = crate::core::user_config::parse_theme_item(v) {
if let Ok(c) = parse_theme_item(v) {
self.user_config.theme.hovered = c;
self.user_config.custom_theme.hovered = c;
}
}
}
"theme.selected" => {
"theme.selected" if self.user_config.current_preset == ThemePreset::Custom => {
if let SettingValue::Color(v) = &setting.value {
if let Ok(c) = crate::core::user_config::parse_theme_item(v) {
if let Ok(c) = parse_theme_item(v) {
self.user_config.theme.selected = c;
self.user_config.custom_theme.selected = c;
}
}
}
"theme.inactive" => {
"theme.inactive" if self.user_config.current_preset == ThemePreset::Custom => {
if let SettingValue::Color(v) = &setting.value {
if let Ok(c) = crate::core::user_config::parse_theme_item(v) {
if let Ok(c) = parse_theme_item(v) {
self.user_config.theme.inactive = c;
self.user_config.custom_theme.inactive = c;
}
}
}
"theme.text" => {
"theme.text" if self.user_config.current_preset == ThemePreset::Custom => {
if let SettingValue::Color(v) = &setting.value {
if let Ok(c) = crate::core::user_config::parse_theme_item(v) {
if let Ok(c) = parse_theme_item(v) {
self.user_config.theme.text = c;
self.user_config.custom_theme.text = c;
}
}
}
"theme.error_text" => {
"theme.error_text" if self.user_config.current_preset == ThemePreset::Custom => {
if let SettingValue::Color(v) = &setting.value {
if let Ok(c) = crate::core::user_config::parse_theme_item(v) {
if let Ok(c) = parse_theme_item(v) {
self.user_config.theme.error_text = c;
self.user_config.custom_theme.error_text = c;
}
}
}
"theme.playbar_background" => {
"theme.playbar_background" if self.user_config.current_preset == ThemePreset::Custom => {
if let SettingValue::Color(v) = &setting.value {
if let Ok(c) = crate::core::user_config::parse_theme_item(v) {
if let Ok(c) = parse_theme_item(v) {
self.user_config.theme.playbar_background = c;
self.user_config.custom_theme.playbar_background = c;
}
}
}
"theme.playbar_progress" => {
"theme.playbar_progress" if self.user_config.current_preset == ThemePreset::Custom => {
if let SettingValue::Color(v) = &setting.value {
if let Ok(c) = crate::core::user_config::parse_theme_item(v) {
if let Ok(c) = parse_theme_item(v) {
self.user_config.theme.playbar_progress = c;
self.user_config.custom_theme.playbar_progress = c;
}
}
}
"theme.highlighted_lyrics" => {
"theme.highlighted_lyrics" if self.user_config.current_preset == ThemePreset::Custom => {
if let SettingValue::Color(v) = &setting.value {
if let Ok(c) = crate::core::user_config::parse_theme_item(v) {
if let Ok(c) = parse_theme_item(v) {
self.user_config.theme.highlighted_lyrics = c;
self.user_config.custom_theme.highlighted_lyrics = c;
}
}
}
Expand Down
55 changes: 37 additions & 18 deletions src/core/user_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const APP_CONFIG_DIR: &str = "spotatui";

#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct UserTheme {
pub preset: Option<String>,
pub active: Option<String>,
pub banner: Option<String>,
pub error_border: Option<String>,
Expand Down Expand Up @@ -122,6 +123,7 @@ impl ThemePreset {
ThemePreset::Gruvbox,
ThemePreset::GruvboxLight,
ThemePreset::CatppuccinMocha,
ThemePreset::Custom,
]
}

Expand Down Expand Up @@ -721,6 +723,8 @@ pub struct UserConfigString {
pub struct UserConfig {
pub keys: KeyBindings,
pub theme: Theme,
pub current_preset: ThemePreset,
pub custom_theme: Theme,
pub behavior: BehaviorConfig,
pub path_to_config: Option<UserConfigPaths>,
}
Expand All @@ -741,6 +745,8 @@ impl UserConfig {

UserConfig {
theme: Default::default(),
current_preset: ThemePreset::Default,
custom_theme: Default::default(),
keys: KeyBindings {
back: Key::Char('q'),
next_page: Key::Ctrl('d'),
Expand Down Expand Up @@ -898,10 +904,12 @@ impl UserConfig {
}

pub fn load_theme(&mut self, theme: UserTheme) -> Result<()> {
// Individual color fields populate the custom_theme — they only
// become the active theme when current_preset is Custom.
macro_rules! to_theme_item {
($name: ident) => {
if let Some(theme_item) = theme.$name {
self.theme.$name = parse_theme_item(&theme_item)?;
self.custom_theme.$name = parse_theme_item(&theme_item)?;
}
};
}
Expand All @@ -922,6 +930,16 @@ impl UserConfig {
to_theme_item!(background);
to_theme_item!(header);
to_theme_item!(highlighted_lyrics);

if let Some(preset_name) = theme.preset {
self.current_preset = ThemePreset::from_name(&preset_name);
}

self.theme = match self.current_preset {
ThemePreset::Custom => self.custom_theme,
preset => preset.to_theme(),
};

Ok(())
}

Expand Down Expand Up @@ -1251,22 +1269,23 @@ impl UserConfig {

// Helper to build theme config from current values
let build_theme = || UserTheme {
active: Some(color_to_string(self.theme.active)),
banner: Some(color_to_string(self.theme.banner)),
error_border: Some(color_to_string(self.theme.error_border)),
error_text: Some(color_to_string(self.theme.error_text)),
hint: Some(color_to_string(self.theme.hint)),
hovered: Some(color_to_string(self.theme.hovered)),
inactive: Some(color_to_string(self.theme.inactive)),
playbar_background: Some(color_to_string(self.theme.playbar_background)),
playbar_progress: Some(color_to_string(self.theme.playbar_progress)),
playbar_progress_text: Some(color_to_string(self.theme.playbar_progress_text)),
playbar_text: Some(color_to_string(self.theme.playbar_text)),
selected: Some(color_to_string(self.theme.selected)),
text: Some(color_to_string(self.theme.text)),
background: Some(color_to_string(self.theme.background)),
header: Some(color_to_string(self.theme.header)),
highlighted_lyrics: Some(color_to_string(self.theme.highlighted_lyrics)),
preset: Some(self.current_preset.name().to_string()),
active: Some(color_to_string(self.custom_theme.active)),
banner: Some(color_to_string(self.custom_theme.banner)),
error_border: Some(color_to_string(self.custom_theme.error_border)),
error_text: Some(color_to_string(self.custom_theme.error_text)),
hint: Some(color_to_string(self.custom_theme.hint)),
hovered: Some(color_to_string(self.custom_theme.hovered)),
inactive: Some(color_to_string(self.custom_theme.inactive)),
playbar_background: Some(color_to_string(self.custom_theme.playbar_background)),
playbar_progress: Some(color_to_string(self.custom_theme.playbar_progress)),
playbar_progress_text: Some(color_to_string(self.custom_theme.playbar_progress_text)),
playbar_text: Some(color_to_string(self.custom_theme.playbar_text)),
selected: Some(color_to_string(self.custom_theme.selected)),
text: Some(color_to_string(self.custom_theme.text)),
background: Some(color_to_string(self.custom_theme.background)),
header: Some(color_to_string(self.custom_theme.header)),
highlighted_lyrics: Some(color_to_string(self.custom_theme.highlighted_lyrics)),
};

// If the file exists, try to read it first to preserve keybindings
Expand Down Expand Up @@ -1364,7 +1383,7 @@ pub fn parse_theme_item(theme_item: &str) -> Result<Color> {
Ok(color)
}

fn color_to_string(color: Color) -> String {
pub fn color_to_string(color: Color) -> String {
match color {
Color::Reset => "Reset".to_string(),
Color::Black => "Black".to_string(),
Expand Down
11 changes: 11 additions & 0 deletions src/tui/handlers/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ fn handle_string_edit(key: Key, app: &mut App) {
}
}

let is_color_edit = matches!(setting.value, SettingValue::Color(_));
match &setting.value {
SettingValue::String(_) => {
setting.value = SettingValue::String(new_value);
Expand All @@ -200,6 +201,16 @@ fn handle_string_edit(key: Key, app: &mut App) {
}
_ => {}
}
// Editing an individual color switches the theme to Custom
if is_color_edit {
if let Some(preset_setting) =
app.settings_items.iter_mut().find(|s| s.id == "theme.preset")
{
if let SettingValue::Preset(name) = &mut preset_setting.value {
*name = "Custom".to_string();
}
}
}
}
app.settings_edit_mode = false;
app.settings_edit_buffer.clear();
Expand Down