@@ -92,6 +92,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
9292 if m .mode == modeImportStations {
9393 return m .updateImportStations (msg )
9494 }
95+ if m .mode == modeThemePicker {
96+ return m .updateThemePicker (msg )
97+ }
9598
9699 switch msg := msg .(type ) {
97100 case tea.WindowSizeMsg :
@@ -229,13 +232,7 @@ func (m Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
229232 return m , setVolumeCmd (m .player , m .volume )
230233
231234 case key .Matches (msg , m .keys .ThemeCycle ):
232- next , _ := theme .Lookup (theme .Next (m .theme .Name ))
233- m .theme = next
234- m .styles = NewStyles (next )
235- // Spinner color is baked at construction; refresh it so the
236- // spinner stays in sync with the active theme's Muted tone.
237- m .spinner .Style = lipgloss .NewStyle ().Foreground (next .Muted )
238- return m , nil
235+ return m .openThemePicker (), nil
239236
240237 case key .Matches (msg , m .keys .Mini ):
241238 if m .mode == modeFull {
@@ -384,6 +381,55 @@ func (m Model) updateImportStations(msg tea.Msg) (tea.Model, tea.Cmd) {
384381 return m , nil
385382}
386383
384+ func (m Model ) openThemePicker () Model {
385+ m .modePrev = m .mode
386+ m .mode = modeThemePicker
387+ m .themeBeforePicker = m .theme .Name
388+ m .themeCursor = m .currentThemeIndex ()
389+ return m
390+ }
391+
392+ func (m Model ) updateThemePicker (msg tea.Msg ) (tea.Model , tea.Cmd ) {
393+ km , ok := msg .(tea.KeyMsg )
394+ if ! ok {
395+ return m , nil
396+ }
397+
398+ names := theme .Names ()
399+ if len (names ) == 0 {
400+ m .mode = m .modePrev
401+ return m , nil
402+ }
403+
404+ move := func (delta int ) {
405+ m .themeCursor = (m .themeCursor + delta + len (names )) % len (names )
406+ m = m .applyTheme (names [m .themeCursor ])
407+ }
408+
409+ switch km .String () {
410+ case "up" , "k" :
411+ move (- 1 )
412+ return m , nil
413+ case "down" , "j" :
414+ move (1 )
415+ return m , nil
416+ case "enter" :
417+ m .mode = m .modePrev
418+ m .themeBeforePicker = ""
419+ return m , nil
420+ case "esc" :
421+ if m .themeBeforePicker != "" {
422+ m = m .applyTheme (m .themeBeforePicker )
423+ }
424+ m .mode = m .modePrev
425+ m .themeBeforePicker = ""
426+ return m , nil
427+ case "q" , "ctrl+c" :
428+ return m , tea .Quit
429+ }
430+ return m , nil
431+ }
432+
387433// updateConfirmDelete handles the delete-confirmation modal. y/Y/enter
388434// commits, n/N/esc cancels. Anything else is ignored so the user can't
389435// accidentally dismiss it by stray keys.
@@ -451,6 +497,25 @@ func (m Model) newStationsOnly(stations []config.Station) ([]config.Station, int
451497 return out , skipped
452498}
453499
500+ func (m Model ) currentThemeIndex () int {
501+ for i , name := range theme .Names () {
502+ if name == m .theme .Name {
503+ return i
504+ }
505+ }
506+ return 0
507+ }
508+
509+ func (m Model ) applyTheme (name string ) Model {
510+ next , _ := theme .Lookup (name )
511+ m .theme = next
512+ m .styles = NewStyles (next )
513+ // Spinner color is baked at construction; refresh it so the
514+ // spinner stays in sync with the active theme's Muted tone.
515+ m .spinner .Style = lipgloss .NewStyle ().Foreground (next .Muted )
516+ return m
517+ }
518+
454519// commitDelete removes the pending station from cfg.Stations,
455520// rewrites the config, and adjusts cursor / playingIdx so the model
456521// stays consistent (deleting the currently-playing station pauses
0 commit comments