Compare commits
3 Commits
e482e3261c
...
648325a690
| Author | SHA1 | Date | |
|---|---|---|---|
| 648325a690 | |||
| 27927f3ab1 | |||
| 276539c383 |
@@ -74,7 +74,7 @@ portable application: moving the program folder also moves its configuration.
|
||||
"execution_mode": "parallel",
|
||||
"overlap_policy": "skip",
|
||||
"default_timeout_seconds": 0,
|
||||
"theme": "default",
|
||||
"theme": "gosentry",
|
||||
"job_list_view": "detailed"
|
||||
}
|
||||
```
|
||||
@@ -126,22 +126,55 @@ include the run timestamp and job name:
|
||||
|
||||
## Schedules
|
||||
|
||||
Interval schedules using Go duration syntax:
|
||||
GoSentry accepts two schedule forms: fixed `@every` intervals and standard
|
||||
5-field cron expressions.
|
||||
|
||||
### `@every` intervals
|
||||
|
||||
Write `@every` followed by a [Go duration](https://pkg.go.dev/time#ParseDuration)
|
||||
— a positive number with a unit suffix. Units can be combined in one value:
|
||||
|
||||
```text
|
||||
@every 10s
|
||||
@every 5m
|
||||
@every 1h30m
|
||||
@every 10s every 10 seconds
|
||||
@every 5m every 5 minutes
|
||||
@every 1h every hour
|
||||
@every 1h30m every hour and a half (same as @every 90m)
|
||||
@every 2h45m10s hours, minutes, and seconds combined
|
||||
```
|
||||
|
||||
Standard 5-field cron expressions:
|
||||
Supported units:
|
||||
|
||||
| Unit | Meaning |
|
||||
|------|---------|
|
||||
| `ns` | nanoseconds |
|
||||
| `us`, `µs` | microseconds |
|
||||
| `ms` | milliseconds |
|
||||
| `s` | seconds |
|
||||
| `m` | minutes |
|
||||
| `h` | hours |
|
||||
|
||||
`@every` does **not** support days, weeks, months, or years — those follow a
|
||||
calendar, not a fixed interval. For “every day at 02:00”, “on the 1st of each
|
||||
month”, or “once a year”, use a cron expression (below).
|
||||
|
||||
The scheduler checks due jobs once per second, so values shorter than `1s` are
|
||||
accepted but will not fire faster than once a second.
|
||||
|
||||
### Cron expressions
|
||||
|
||||
Five fields: minute, hour, day-of-month, month, day-of-week.
|
||||
|
||||
```text
|
||||
*/5 * * * * every five minutes
|
||||
0 2 * * * every day at 02:00
|
||||
30 9 * * 1-5 weekdays at 09:30
|
||||
0 0 1 * * first day of every month at midnight
|
||||
0 0 1 1 * every year on 1 January at midnight
|
||||
```
|
||||
|
||||
Named descriptors are also accepted: `@hourly`, `@daily`, `@weekly`,
|
||||
`@monthly`, `@yearly` (and `@annually`, `@midnight`).
|
||||
|
||||
## Using The App
|
||||
|
||||
1. Start GoSentry.
|
||||
|
||||
@@ -38,6 +38,10 @@ dragged.**
|
||||
|
||||
**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
|
||||
its layout always intended, rather than 8.
|
||||
- The caption column is as wide as the widest caption instead of a fixed width,
|
||||
@@ -51,9 +55,16 @@ dragged.**
|
||||
the rows above have to give but a dropdown — which paints its box out to the
|
||||
row's edge — does not, so the gap collapsed to about a pixel. The Theme row
|
||||
now keeps the same gap the checkbox rows have.
|
||||
- The **About** repository link points at GitHub (`mixeme/gosentry`) instead of
|
||||
the private Gitea mirror.
|
||||
|
||||
**Documentation:**
|
||||
|
||||
- The **README Schedules** section now documents `@every` in full: supported Go
|
||||
duration units (`ns` through `h`), combined values such as `1h30m`, the link to
|
||||
`time.ParseDuration`, the fact that days/months/years belong in cron rather
|
||||
than `@every`, the one-second scheduler tick floor, cron examples for monthly
|
||||
and yearly runs, and the `@hourly`/`@daily`/… descriptors.
|
||||
- The **README** describes the application that exists. Its `gosentry.json`
|
||||
sample was three keys short of what the app writes on first run, which made
|
||||
the one file the user is invited to hand-edit the least accurate thing in the
|
||||
|
||||
+1
-1
@@ -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. |
|
||||
| `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. |
|
||||
|
||||
---
|
||||
|
||||
@@ -481,8 +481,8 @@ func validateConfig(config domain.Config) error {
|
||||
if config.DefaultTimeoutSeconds < 0 {
|
||||
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
|
||||
// configs (and hand-built ones) stay valid without an explicit theme.
|
||||
// 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'")
|
||||
}
|
||||
|
||||
@@ -87,8 +87,8 @@ type Config struct {
|
||||
// omitempty would hide a deliberate choice from the hand-editable config.
|
||||
DefaultTimeoutSeconds int `json:"default_timeout_seconds"`
|
||||
Paused bool `json:"paused,omitempty"`
|
||||
// Theme selects the visual appearance. Empty is treated as ThemeDefault so
|
||||
// configs written before this field existed keep the original look.
|
||||
// Theme selects the visual appearance. Empty is treated as ThemeGoSentry so
|
||||
// configs written before this field existed pick up the branded look.
|
||||
Theme Theme `json:"theme,omitempty"`
|
||||
// JobListView selects the Jobs list density. Empty is treated as
|
||||
// JobListViewDetailed so configs written before this field existed keep the
|
||||
@@ -110,7 +110,7 @@ func DefaultConfig() Config {
|
||||
NotifyOnFailure: true,
|
||||
ExecutionMode: ExecutionModeParallel,
|
||||
OverlapPolicy: OverlapPolicySkip,
|
||||
Theme: ThemeDefault,
|
||||
Theme: ThemeGoSentry,
|
||||
JobListView: JobListViewDetailed,
|
||||
DefaultTimeoutSeconds: 0,
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ func loadOrCreateConfig(paths Paths) (domain.Config, error) {
|
||||
// the setting impossible to persist. Negative values are rejected by
|
||||
// app.validateConfig before they can be saved.
|
||||
if config.Theme == "" {
|
||||
config.Theme = domain.ThemeDefault
|
||||
config.Theme = domain.ThemeGoSentry
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
@@ -171,8 +171,8 @@ func TestLoadOrCreateConfigCreatesDefaultsOnFirstRun(t *testing.T) {
|
||||
if got.DefaultTimeoutSeconds != 0 {
|
||||
t.Errorf("default DefaultTimeoutSeconds = %d, want 0 (no timeout)", got.DefaultTimeoutSeconds)
|
||||
}
|
||||
if got.Theme != domain.ThemeDefault {
|
||||
t.Errorf("default Theme = %q, want %q", got.Theme, domain.ThemeDefault)
|
||||
if got.Theme != domain.ThemeGoSentry {
|
||||
t.Errorf("default Theme = %q, want %q", got.Theme, domain.ThemeGoSentry)
|
||||
}
|
||||
if got.JobListView != domain.JobListViewDetailed {
|
||||
t.Errorf("default JobListView = %q, want %q", got.JobListView, domain.JobListViewDetailed)
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
const projectRepositoryURL = "https://gitea.mixdep.ru/mix/gosentry"
|
||||
const projectRepositoryURL = "https://github.com/mixeme/gosentry"
|
||||
|
||||
// settingsCaptions lists every settingsRow caption in the tab, in no
|
||||
// particular order. settingsView measures this once with captionColumnWidth
|
||||
@@ -263,10 +263,10 @@ const (
|
||||
)
|
||||
|
||||
func themeLabel(choice domain.Theme) string {
|
||||
if choice == domain.ThemeGoSentry {
|
||||
return themeLabelGoSentry
|
||||
}
|
||||
if choice == domain.ThemeDefault {
|
||||
return themeLabelDefault
|
||||
}
|
||||
return themeLabelGoSentry
|
||||
}
|
||||
|
||||
func themeFromLabel(label string) domain.Theme {
|
||||
|
||||
+6
-6
@@ -103,14 +103,14 @@ 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) Size(name fyne.ThemeSizeName) float32 { return t.base.Size(name) }
|
||||
|
||||
// themeFor maps a stored Theme choice to a concrete fyne.Theme. Anything other
|
||||
// than the explicit GoSentry choice (including the empty/legacy value) keeps
|
||||
// Fyne's built-in theme.
|
||||
// themeFor maps a stored Theme choice to a concrete fyne.Theme. Only the
|
||||
// explicit default 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.ThemeGoSentry {
|
||||
return newGoSentryTheme()
|
||||
}
|
||||
if choice == domain.ThemeDefault {
|
||||
return theme.DefaultTheme()
|
||||
}
|
||||
return newGoSentryTheme()
|
||||
}
|
||||
|
||||
// applyTheme installs the theme for the given choice on the running app. Fyne
|
||||
|
||||
+13
-13
@@ -54,24 +54,24 @@ func TestGoSentryThemeDelegatesUnbrandedColors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// themeFor maps the stored choice to the right theme: the GoSentry choice yields
|
||||
// the branded teal primary; every other value (including the empty legacy value)
|
||||
// yields the default theme, whose primary is not the brand teal.
|
||||
// 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
|
||||
// choice yields Fyne's built-in theme.
|
||||
func TestThemeForChoice(t *testing.T) {
|
||||
gosentry := themeFor(domain.ThemeGoSentry)
|
||||
if got := gosentry.Color(theme.ColorNamePrimary, theme.VariantLight); got != brandTeal {
|
||||
t.Errorf("themeFor(gosentry) primary = %v, want brand teal %v", got, brandTeal)
|
||||
for _, choice := range []domain.Theme{domain.ThemeGoSentry, ""} {
|
||||
branded := themeFor(choice)
|
||||
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 {
|
||||
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
|
||||
// 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) {
|
||||
if got := themeFromLabel(themeLabel(domain.ThemeGoSentry)); got != domain.ThemeGoSentry {
|
||||
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 {
|
||||
t.Errorf("round-trip default = %q", got)
|
||||
}
|
||||
if got := themeLabel(""); got != themeLabelDefault {
|
||||
t.Errorf("empty theme label = %q, want %q", got, themeLabelDefault)
|
||||
if got := themeLabel(""); got != themeLabelGoSentry {
|
||||
t.Errorf("empty theme label = %q, want %q", got, themeLabelGoSentry)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user