diff --git a/README.md b/README.md index 5dec016..19b5170 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ creating, grouping, pausing, running, and monitoring scheduled shell commands. - Desktop notifications on job failure. - Windows tray icon: left-click to show the window, right-click for the menu. - Autostart on login (Windows shortcut; Linux XDG desktop entry). -- Detailed or compact job list, and a default or branded theme; both are remembered. +- Detailed or compact job list, and a system or branded theme; both are remembered. ## Platforms @@ -83,7 +83,7 @@ That is the file GoSentry writes on first run. `default_timeout_seconds` is the run timeout applied to jobs that do not set their own; `0` means no timeout, and it is written out even though it is zero, because a missing value and a deliberate "no timeout" have to stay distinguishable in a hand-edited file. -`theme` is `default` or `gosentry` (the branded teal/amber look), and +`theme` is `system` or `gosentry` (the branded teal/amber look), and `job_list_view` is `detailed` or `compact` — both are remembered from the choices made in the app. Keys left at their off value (`start_on_login`, `paused`) are omitted until they are turned on. diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 6ecfe09..76dadf5 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -40,8 +40,10 @@ dragged.** - **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 + look; users who prefer Fyne's built-in theme can still pick **System** in Settings. +- The Fyne built-in theme option is labelled **System** (stored as `"system"`); + configs that still say `"default"` are read as System and rewritten on save. - The **Save / Cancel / Restore defaults** row sits 4 px from the left edge, as its layout always intended, rather than 8. - The caption column is as wide as the widest caption instead of a fixed width, diff --git a/docs/TESTS.md b/docs/TESTS.md index 8995df1..56159e1 100644 --- a/docs/TESTS.md +++ b/docs/TESTS.md @@ -237,6 +237,7 @@ Tests JSON round-tripping, default generation, and backward compatibility. | `TestLoadOrCreateConfigCreatesDefaultsOnFirstRun` | Verifies that a missing config file is created with sane defaults and a sample job. | | `TestLoadOrCreateConfigKeepsZeroTimeoutOnReload` | Verifies that `default_timeout_seconds: 0` survives a reload rather than being normalized away — 0 is a value, not a missing field. | | `TestLoadOrCreateConfigMigratesJobsDir` | Verifies that a pre-0.15 `jobs_dir` becomes `jobs_file` pointing at the same `jobs.json`, and that the retired key is not written back. | +| `TestLoadOrCreateConfigMigratesLegacyThemeDefault` | Verifies that a config storing the retired `"default"` theme value is normalized to `system` on load. | | `TestLoadJobsFileReportsMissingWithoutCreating` | Verifies that `LoadJobsFile` reports a missing file as not-found without creating or seeding it, and normalizes the jobs it does load. | | `TestApplyConfigPathsDerivesJobsDir` | Verifies that the configured jobs file resolves against the program folder and that `Paths.JobsDir` is derived from it. | | `TestJobTimeoutRoundTripsThreeStates` | Verifies the on-disk encoding that keeps "inherit" and "no timeout" distinguishable: `nil` is omitted entirely, an explicit `0` is written and read back as set. | @@ -514,8 +515,8 @@ 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. | | `TestGoSentryThemeDelegatesUnbrandedColors` | Verifies unbranded color names fall through to the base theme rather than rendering transparent. | -| `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. | +| `TestThemeForChoice` | Verifies the GoSentry choice and the empty legacy value yield the branded primary; only the explicit system choice yields Fyne's built-in theme. | +| `TestThemeLabelRoundTrip` | Verifies the dropdown labels round-trip and that the empty value maps to the GoSentry label rather than a blank option. | --- diff --git a/src/app/operations.go b/src/app/operations.go index a6cb9eb..5137075 100644 --- a/src/app/operations.go +++ b/src/app/operations.go @@ -483,8 +483,8 @@ func validateConfig(config domain.Config) error { } // Empty Theme is accepted and normalized to the branded theme on load, so // older configs (and hand-built ones) stay valid without an explicit theme. - if config.Theme != "" && config.Theme != domain.ThemeDefault && config.Theme != domain.ThemeGoSentry { - return errors.New("theme must be 'default' or 'gosentry'") + if config.Theme != "" && config.Theme != domain.ThemeSystem && config.Theme != domain.ThemeGoSentry { + return errors.New("theme must be 'system' or 'gosentry'") } return nil } diff --git a/src/domain/config.go b/src/domain/config.go index bfb1643..a83175b 100644 --- a/src/domain/config.go +++ b/src/domain/config.go @@ -21,8 +21,8 @@ const ( type Theme string const ( - // ThemeDefault keeps Fyne's built-in theme — the original look. - ThemeDefault Theme = "default" + // ThemeSystem keeps Fyne's built-in theme, following the platform look. + ThemeSystem Theme = "system" // ThemeGoSentry applies the branded teal/amber theme derived from the logo // and app icon. ThemeGoSentry Theme = "gosentry" diff --git a/src/storage/store.go b/src/storage/store.go index 3c87190..e35c685 100644 --- a/src/storage/store.go +++ b/src/storage/store.go @@ -120,6 +120,9 @@ func loadOrCreateConfig(paths Paths) (domain.Config, error) { if config.Theme == "" { config.Theme = domain.ThemeGoSentry } + if config.Theme == "default" { + config.Theme = domain.ThemeSystem + } return config, nil } diff --git a/src/storage/store_test.go b/src/storage/store_test.go index 905f77f..a07b1f4 100644 --- a/src/storage/store_test.go +++ b/src/storage/store_test.go @@ -183,6 +183,34 @@ func TestLoadOrCreateConfigCreatesDefaultsOnFirstRun(t *testing.T) { } } +// TestLoadOrCreateConfigMigratesLegacyThemeDefault covers a gosentry.json that +// still stores the retired "default" theme value: load normalizes it to system. +func TestLoadOrCreateConfigMigratesLegacyThemeDefault(t *testing.T) { + dir := t.TempDir() + paths := Paths{ + AppDir: dir, + ConfigPath: filepath.Join(dir, ConfigFileName), + } + legacy := map[string]any{ + "jobs_file": "jobs.json", + "logs_dir": "logs", + "max_log_files": 100, + "max_log_age_days": 30, + "theme": "default", + } + if err := writeJSON(paths.ConfigPath, legacy); err != nil { + t.Fatal(err) + } + + got, err := loadOrCreateConfig(paths) + if err != nil { + t.Fatal(err) + } + if got.Theme != domain.ThemeSystem { + t.Errorf("migrated Theme = %q, want %q", got.Theme, domain.ThemeSystem) + } +} + // TestLoadOrCreateConfigKeepsZeroTimeoutOnReload guards the "0 = no timeout" // setting against being normalized away when an existing gosentry.json is read // back. Loading must not treat 0 as a missing value. diff --git a/src/ui/settings_view.go b/src/ui/settings_view.go index f34cf86..73afb4f 100644 --- a/src/ui/settings_view.go +++ b/src/ui/settings_view.go @@ -61,7 +61,7 @@ func settingsView(w fyne.Window, svc *app.Service) fyne.CanvasObject { notifications := widget.NewCheck("Show desktop notifications for failed jobs", nil) notifications.SetChecked(store.Config.NotifyOnFailure) notifications.OnChanged = func(bool) { updateSaveState() } - themeSelect := widget.NewSelect([]string{themeLabelDefault, themeLabelGoSentry}, nil) + themeSelect := widget.NewSelect([]string{themeLabelSystem, themeLabelGoSentry}, nil) themeSelect.SetSelected(themeLabel(store.Config.Theme)) // Preview the theme the moment it is picked so the choice is visible before // saving; Save persists it. Reverting the selection reverts the preview, and @@ -256,15 +256,15 @@ func settingsView(w fyne.Window, svc *app.Service) fyne.CanvasObject { // Theme dropdown labels. These are the human-facing captions; themeLabel and // themeFromLabel translate between them and the stored domain.Theme values so the -// select never leaks the on-disk "default"/"gosentry" strings to the user. +// select never leaks the on-disk "system"/"gosentry" strings to the user. const ( - themeLabelDefault = "Default" + themeLabelSystem = "System" themeLabelGoSentry = "GoSentry" ) func themeLabel(choice domain.Theme) string { - if choice == domain.ThemeDefault { - return themeLabelDefault + if choice == domain.ThemeSystem { + return themeLabelSystem } return themeLabelGoSentry } @@ -273,5 +273,5 @@ func themeFromLabel(label string) domain.Theme { if label == themeLabelGoSentry { return domain.ThemeGoSentry } - return domain.ThemeDefault + return domain.ThemeSystem } diff --git a/src/ui/theme.go b/src/ui/theme.go index 2e382ff..192c340 100644 --- a/src/ui/theme.go +++ b/src/ui/theme.go @@ -104,10 +104,10 @@ func (t gosentryTheme) Icon(name fyne.ThemeIconName) fyne.Resource { return t.ba func (t gosentryTheme) Size(name fyne.ThemeSizeName) float32 { return t.base.Size(name) } // themeFor maps a stored Theme choice to a concrete fyne.Theme. Only the -// explicit default choice keeps Fyne's built-in theme; everything else +// explicit system choice keeps Fyne's built-in theme; everything else // (including the empty/legacy value) uses the branded GoSentry theme. func themeFor(choice domain.Theme) fyne.Theme { - if choice == domain.ThemeDefault { + if choice == domain.ThemeSystem { return theme.DefaultTheme() } return newGoSentryTheme() diff --git a/src/ui/theme_test.go b/src/ui/theme_test.go index 723d58a..e28dc06 100644 --- a/src/ui/theme_test.go +++ b/src/ui/theme_test.go @@ -55,7 +55,7 @@ func TestGoSentryThemeDelegatesUnbrandedColors(t *testing.T) { } // themeFor maps the stored choice to the right theme: the GoSentry choice and the -// empty legacy value yield the branded teal primary; only the explicit default +// empty legacy value yield the branded teal primary; only the explicit system // choice yields Fyne's built-in theme. func TestThemeForChoice(t *testing.T) { for _, choice := range []domain.Theme{domain.ThemeGoSentry, ""} { @@ -64,9 +64,9 @@ func TestThemeForChoice(t *testing.T) { t.Errorf("themeFor(%q) primary = %v, want brand teal %v", choice, got, brandTeal) } } - def := themeFor(domain.ThemeDefault) - if got := def.Color(theme.ColorNamePrimary, theme.VariantLight); got == brandTeal { - t.Errorf("themeFor(default) should not use the brand teal primary") + sys := themeFor(domain.ThemeSystem) + if got := sys.Color(theme.ColorNamePrimary, theme.VariantLight); got == brandTeal { + t.Errorf("themeFor(system) should not use the brand teal primary") } } @@ -76,8 +76,8 @@ func TestThemeLabelRoundTrip(t *testing.T) { if got := themeFromLabel(themeLabel(domain.ThemeGoSentry)); got != domain.ThemeGoSentry { t.Errorf("round-trip gosentry = %q", got) } - if got := themeFromLabel(themeLabel(domain.ThemeDefault)); got != domain.ThemeDefault { - t.Errorf("round-trip default = %q", got) + if got := themeFromLabel(themeLabel(domain.ThemeSystem)); got != domain.ThemeSystem { + t.Errorf("round-trip system = %q", got) } if got := themeLabel(""); got != themeLabelGoSentry { t.Errorf("empty theme label = %q, want %q", got, themeLabelGoSentry)