feat: add theme_mode, theme_light, theme_dark to UserAppearanceSettings (#25076)

Part 1: Backend portion of a change broken into 2 PRs.
Part 2: #25077 

Adds three new UserAppearanceSettings fields (theme_mode, theme_light,
theme_dark) on top of the existing theme_preference and terminal_font.
Replaces GetUserThemePreference and GetUserTerminalFont with a single
GetUserAppearanceSettings aggregate query. The PUT handler is wrapped in
db.InTx so sync-mode's mode + slot writes can never half-apply.
This commit is contained in:
Jaayden Halko
2026-05-14 05:44:05 +01:00
committed by GitHub
parent d147dd3bdd
commit 024132e8a4
23 changed files with 1209 additions and 223 deletions
+102 -44
View File
@@ -1124,35 +1124,38 @@ func (api *API) userAppearanceSettings(rw http.ResponseWriter, r *http.Request)
user = httpmw.UserParam(r)
)
themePreference, err := api.Database.GetUserThemePreference(ctx, user.ID)
settings, err := api.Database.GetUserAppearanceSettings(ctx, user.ID)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Error reading user settings.",
Detail: err.Error(),
})
return
}
themePreference = ""
writeUserSettingsReadError(ctx, rw, err)
return
}
terminalFont, err := api.Database.GetUserTerminalFont(ctx, user.ID)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Error reading user settings.",
Detail: err.Error(),
})
return
}
httpapi.Write(ctx, rw, http.StatusOK, userAppearanceSettingsFromRow(settings))
}
terminalFont = ""
func userAppearanceSettingsFromRow(settings database.GetUserAppearanceSettingsRow) codersdk.UserAppearanceSettings {
return codersdk.UserAppearanceSettings{
ThemePreference: settings.ThemePreference,
ThemeMode: codersdk.ThemeMode(settings.ThemeMode),
ThemeLight: settings.ThemeLight,
ThemeDark: settings.ThemeDark,
TerminalFont: codersdk.TerminalFontName(settings.TerminalFont),
}
}
httpapi.Write(ctx, rw, http.StatusOK, codersdk.UserAppearanceSettings{
ThemePreference: themePreference,
TerminalFont: codersdk.TerminalFontName(terminalFont),
func isLegacyAutoThemePreference(themePreference string) bool {
switch themePreference {
case "auto", "auto-protan-deuter", "auto-tritan":
return true
default:
return false
}
}
func writeUserSettingsReadError(ctx context.Context, rw http.ResponseWriter, err error) {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Error reading user settings.",
Detail: err.Error(),
})
}
@@ -1184,34 +1187,89 @@ func (api *API) putUserAppearanceSettings(rw http.ResponseWriter, r *http.Reques
return
}
updatedThemePreference, err := api.Database.UpdateUserThemePreference(ctx, database.UpdateUserThemePreferenceParams{
UserID: user.ID,
ThemePreference: params.ThemePreference,
})
// theme_mode is optional for backward compatibility. Older CLI
// clients do not know about theme_mode or the sync slots, so an
// omitted mode must leave those fields untouched instead of replacing
// them with single-mode defaults. Legacy auto values are the exception:
// the old UI used them to mean sync-with-system, so clearing theme_mode
// lets modern clients migrate them on read.
themeModeProvided := params.ThemeMode != codersdk.ThemeModeUnset
updateThemeMode := themeModeProvided
isSyncMode := params.ThemeMode == codersdk.ThemeModeSync
isSingleMode := params.ThemeMode == codersdk.ThemeModeSingle
updateThemeLight := isSyncMode || (isSingleMode && params.ThemeLight != "")
updateThemeDark := isSyncMode || (isSingleMode && params.ThemeDark != "")
themeMode := params.ThemeMode
if !updateThemeMode && isLegacyAutoThemePreference(params.ThemePreference) {
updateThemeMode = true
themeMode = codersdk.ThemeModeUnset
}
var updatedSettings database.GetUserAppearanceSettingsRow
err := api.Database.InTx(func(tx database.Store) error {
_, err := tx.UpdateUserThemePreference(ctx, database.UpdateUserThemePreferenceParams{
UserID: user.ID,
ThemePreference: params.ThemePreference,
})
if err != nil {
return xerrors.Errorf("update user theme preference: %w", err)
}
if updateThemeMode {
_, err = tx.UpdateUserThemeMode(ctx, database.UpdateUserThemeModeParams{
UserID: user.ID,
ThemeMode: string(themeMode),
})
if err != nil {
return xerrors.Errorf("update user theme mode: %w", err)
}
}
if updateThemeLight {
_, err = tx.UpdateUserThemeLight(ctx, database.UpdateUserThemeLightParams{
UserID: user.ID,
ThemeLight: params.ThemeLight,
})
if err != nil {
return xerrors.Errorf("update user theme light: %w", err)
}
}
if updateThemeDark {
_, err = tx.UpdateUserThemeDark(ctx, database.UpdateUserThemeDarkParams{
UserID: user.ID,
ThemeDark: params.ThemeDark,
})
if err != nil {
return xerrors.Errorf("update user theme dark: %w", err)
}
}
_, err = tx.UpdateUserTerminalFont(ctx, database.UpdateUserTerminalFontParams{
UserID: user.ID,
TerminalFont: string(params.TerminalFont),
})
if err != nil {
return xerrors.Errorf("update user terminal font: %w", err)
}
updatedSettings, err = tx.GetUserAppearanceSettings(ctx, user.ID)
if err != nil {
return xerrors.Errorf("get updated user appearance settings: %w", err)
}
return nil
}, nil)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error updating user theme preference.",
Message: "Internal error updating user appearance settings.",
Detail: err.Error(),
})
return
}
updatedTerminalFont, err := api.Database.UpdateUserTerminalFont(ctx, database.UpdateUserTerminalFontParams{
UserID: user.ID,
TerminalFont: string(params.TerminalFont),
})
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error updating user terminal font.",
Detail: err.Error(),
})
return
}
httpapi.Write(ctx, rw, http.StatusOK, codersdk.UserAppearanceSettings{
ThemePreference: updatedThemePreference.Value,
TerminalFont: codersdk.TerminalFontName(updatedTerminalFont.Value),
})
httpapi.Write(ctx, rw, http.StatusOK, userAppearanceSettingsFromRow(updatedSettings))
}
// @Summary Get user preference settings