Make the branded GoSentry theme the default.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-04 19:18:48 +03:00
parent e482e3261c
commit 276539c383
10 changed files with 37 additions and 33 deletions
+1 -1
View File
@@ -74,7 +74,7 @@ portable application: moving the program folder also moves its configuration.
"execution_mode": "parallel", "execution_mode": "parallel",
"overlap_policy": "skip", "overlap_policy": "skip",
"default_timeout_seconds": 0, "default_timeout_seconds": 0,
"theme": "default", "theme": "gosentry",
"job_list_view": "detailed" "job_list_view": "detailed"
} }
``` ```
+4
View File
@@ -38,6 +38,10 @@ dragged.**
**Settings:** **Settings:**
- **The branded GoSentry theme is now the default.** Fresh installs, the
**Defaults** button, and configs that omit `theme` all open in the teal/amber
look; users who prefer Fyne's built-in theme can still pick **Default** in
Settings.
- The **Save / Cancel / Restore defaults** row sits 4 px from the left edge, as - The **Save / Cancel / Restore defaults** row sits 4 px from the left edge, as
its layout always intended, rather than 8. its layout always intended, rather than 8.
- The caption column is as wide as the widest caption instead of a fixed width, - The caption column is as wide as the widest caption instead of a fixed width,
+1 -1
View File
@@ -514,7 +514,7 @@ Tests the branded theme and the stored theme choice.
|------|---------| |------|---------|
| `TestGoSentryThemeBrandColors` | Verifies the brand colors land on the semantically correct `ColorName`s in both the light and dark variants. | | `TestGoSentryThemeBrandColors` | Verifies the brand colors land on the semantically correct `ColorName`s in both the light and dark variants. |
| `TestGoSentryThemeDelegatesUnbrandedColors` | Verifies unbranded color names fall through to the base theme rather than rendering transparent. | | `TestGoSentryThemeDelegatesUnbrandedColors` | Verifies unbranded color names fall through to the base theme rather than rendering transparent. |
| `TestThemeForChoice` | Verifies the GoSentry choice yields the branded primary and every other value — including the empty legacy one — yields the default theme. | | `TestThemeForChoice` | Verifies the GoSentry choice and the empty legacy value yield the branded primary; only the explicit default choice yields Fyne's built-in theme. |
| `TestThemeLabelRoundTrip` | Verifies the dropdown labels round-trip and that the empty value maps to the Default label rather than a blank option. | | `TestThemeLabelRoundTrip` | Verifies the dropdown labels round-trip and that the empty value maps to the Default label rather than a blank option. |
--- ---
+2 -2
View File
@@ -481,8 +481,8 @@ func validateConfig(config domain.Config) error {
if config.DefaultTimeoutSeconds < 0 { if config.DefaultTimeoutSeconds < 0 {
return errors.New("default timeout must not be negative (0 means no timeout)") return errors.New("default timeout must not be negative (0 means no timeout)")
} }
// Empty Theme is accepted and normalized to the default on load, so older // Empty Theme is accepted and normalized to the branded theme on load, so
// configs (and hand-built ones) stay valid without an explicit theme. // older configs (and hand-built ones) stay valid without an explicit theme.
if config.Theme != "" && config.Theme != domain.ThemeDefault && config.Theme != domain.ThemeGoSentry { if config.Theme != "" && config.Theme != domain.ThemeDefault && config.Theme != domain.ThemeGoSentry {
return errors.New("theme must be 'default' or 'gosentry'") return errors.New("theme must be 'default' or 'gosentry'")
} }
+3 -3
View File
@@ -87,8 +87,8 @@ type Config struct {
// omitempty would hide a deliberate choice from the hand-editable config. // omitempty would hide a deliberate choice from the hand-editable config.
DefaultTimeoutSeconds int `json:"default_timeout_seconds"` DefaultTimeoutSeconds int `json:"default_timeout_seconds"`
Paused bool `json:"paused,omitempty"` Paused bool `json:"paused,omitempty"`
// Theme selects the visual appearance. Empty is treated as ThemeDefault so // Theme selects the visual appearance. Empty is treated as ThemeGoSentry so
// configs written before this field existed keep the original look. // configs written before this field existed pick up the branded look.
Theme Theme `json:"theme,omitempty"` Theme Theme `json:"theme,omitempty"`
// JobListView selects the Jobs list density. Empty is treated as // JobListView selects the Jobs list density. Empty is treated as
// JobListViewDetailed so configs written before this field existed keep the // JobListViewDetailed so configs written before this field existed keep the
@@ -110,7 +110,7 @@ func DefaultConfig() Config {
NotifyOnFailure: true, NotifyOnFailure: true,
ExecutionMode: ExecutionModeParallel, ExecutionMode: ExecutionModeParallel,
OverlapPolicy: OverlapPolicySkip, OverlapPolicy: OverlapPolicySkip,
Theme: ThemeDefault, Theme: ThemeGoSentry,
JobListView: JobListViewDetailed, JobListView: JobListViewDetailed,
DefaultTimeoutSeconds: 0, DefaultTimeoutSeconds: 0,
} }
+1 -1
View File
@@ -118,7 +118,7 @@ func loadOrCreateConfig(paths Paths) (domain.Config, error) {
// the setting impossible to persist. Negative values are rejected by // the setting impossible to persist. Negative values are rejected by
// app.validateConfig before they can be saved. // app.validateConfig before they can be saved.
if config.Theme == "" { if config.Theme == "" {
config.Theme = domain.ThemeDefault config.Theme = domain.ThemeGoSentry
} }
return config, nil return config, nil
} }
+2 -2
View File
@@ -171,8 +171,8 @@ func TestLoadOrCreateConfigCreatesDefaultsOnFirstRun(t *testing.T) {
if got.DefaultTimeoutSeconds != 0 { if got.DefaultTimeoutSeconds != 0 {
t.Errorf("default DefaultTimeoutSeconds = %d, want 0 (no timeout)", got.DefaultTimeoutSeconds) t.Errorf("default DefaultTimeoutSeconds = %d, want 0 (no timeout)", got.DefaultTimeoutSeconds)
} }
if got.Theme != domain.ThemeDefault { if got.Theme != domain.ThemeGoSentry {
t.Errorf("default Theme = %q, want %q", got.Theme, domain.ThemeDefault) t.Errorf("default Theme = %q, want %q", got.Theme, domain.ThemeGoSentry)
} }
if got.JobListView != domain.JobListViewDetailed { if got.JobListView != domain.JobListViewDetailed {
t.Errorf("default JobListView = %q, want %q", got.JobListView, domain.JobListViewDetailed) t.Errorf("default JobListView = %q, want %q", got.JobListView, domain.JobListViewDetailed)
+3 -3
View File
@@ -263,11 +263,11 @@ const (
) )
func themeLabel(choice domain.Theme) string { func themeLabel(choice domain.Theme) string {
if choice == domain.ThemeGoSentry { if choice == domain.ThemeDefault {
return themeLabelGoSentry
}
return themeLabelDefault return themeLabelDefault
} }
return themeLabelGoSentry
}
func themeFromLabel(label string) domain.Theme { func themeFromLabel(label string) domain.Theme {
if label == themeLabelGoSentry { if label == themeLabelGoSentry {
+6 -6
View File
@@ -103,15 +103,15 @@ func (t gosentryTheme) Font(style fyne.TextStyle) fyne.Resource { return t.base.
func (t gosentryTheme) Icon(name fyne.ThemeIconName) fyne.Resource { return t.base.Icon(name) } func (t gosentryTheme) Icon(name fyne.ThemeIconName) fyne.Resource { return t.base.Icon(name) }
func (t gosentryTheme) Size(name fyne.ThemeSizeName) float32 { return t.base.Size(name) } func (t gosentryTheme) Size(name fyne.ThemeSizeName) float32 { return t.base.Size(name) }
// themeFor maps a stored Theme choice to a concrete fyne.Theme. Anything other // themeFor maps a stored Theme choice to a concrete fyne.Theme. Only the
// than the explicit GoSentry choice (including the empty/legacy value) keeps // explicit default choice keeps Fyne's built-in theme; everything else
// Fyne's built-in theme. // (including the empty/legacy value) uses the branded GoSentry theme.
func themeFor(choice domain.Theme) fyne.Theme { func themeFor(choice domain.Theme) fyne.Theme {
if choice == domain.ThemeGoSentry { if choice == domain.ThemeDefault {
return newGoSentryTheme()
}
return theme.DefaultTheme() return theme.DefaultTheme()
} }
return newGoSentryTheme()
}
// applyTheme installs the theme for the given choice on the running app. Fyne // applyTheme installs the theme for the given choice on the running app. Fyne
// refreshes every canvas when the theme changes, so this works both at startup // refreshes every canvas when the theme changes, so this works both at startup
+13 -13
View File
@@ -54,24 +54,24 @@ func TestGoSentryThemeDelegatesUnbrandedColors(t *testing.T) {
} }
} }
// themeFor maps the stored choice to the right theme: the GoSentry choice yields // themeFor maps the stored choice to the right theme: the GoSentry choice and the
// the branded teal primary; every other value (including the empty legacy value) // empty legacy value yield the branded teal primary; only the explicit default
// yields the default theme, whose primary is not the brand teal. // choice yields Fyne's built-in theme.
func TestThemeForChoice(t *testing.T) { func TestThemeForChoice(t *testing.T) {
gosentry := themeFor(domain.ThemeGoSentry) for _, choice := range []domain.Theme{domain.ThemeGoSentry, ""} {
if got := gosentry.Color(theme.ColorNamePrimary, theme.VariantLight); got != brandTeal { branded := themeFor(choice)
t.Errorf("themeFor(gosentry) primary = %v, want brand teal %v", got, brandTeal) if got := branded.Color(theme.ColorNamePrimary, theme.VariantLight); got != brandTeal {
t.Errorf("themeFor(%q) primary = %v, want brand teal %v", choice, got, brandTeal)
} }
for _, choice := range []domain.Theme{domain.ThemeDefault, ""} { }
def := themeFor(choice) def := themeFor(domain.ThemeDefault)
if got := def.Color(theme.ColorNamePrimary, theme.VariantLight); got == brandTeal { if got := def.Color(theme.ColorNamePrimary, theme.VariantLight); got == brandTeal {
t.Errorf("themeFor(%q) should not use the brand teal primary", choice) t.Errorf("themeFor(default) should not use the brand teal primary")
}
} }
} }
// The dropdown label helpers must round-trip, and the empty/legacy value must map // The dropdown label helpers must round-trip, and the empty/legacy value must map
// to the Default label so the select never shows a blank option. // to the GoSentry label so the select never shows a blank option.
func TestThemeLabelRoundTrip(t *testing.T) { func TestThemeLabelRoundTrip(t *testing.T) {
if got := themeFromLabel(themeLabel(domain.ThemeGoSentry)); got != domain.ThemeGoSentry { if got := themeFromLabel(themeLabel(domain.ThemeGoSentry)); got != domain.ThemeGoSentry {
t.Errorf("round-trip gosentry = %q", got) t.Errorf("round-trip gosentry = %q", got)
@@ -79,7 +79,7 @@ func TestThemeLabelRoundTrip(t *testing.T) {
if got := themeFromLabel(themeLabel(domain.ThemeDefault)); got != domain.ThemeDefault { if got := themeFromLabel(themeLabel(domain.ThemeDefault)); got != domain.ThemeDefault {
t.Errorf("round-trip default = %q", got) t.Errorf("round-trip default = %q", got)
} }
if got := themeLabel(""); got != themeLabelDefault { if got := themeLabel(""); got != themeLabelGoSentry {
t.Errorf("empty theme label = %q, want %q", got, themeLabelDefault) t.Errorf("empty theme label = %q, want %q", got, themeLabelGoSentry)
} }
} }