From 21a93517ebc07c95296625c0d85ef0f8ca01a470 Mon Sep 17 00:00:00 2001 From: mixeme Date: Mon, 22 Jun 2026 00:00:24 +0300 Subject: [PATCH] T4.7: Remove forbidden platform imports from src/ui src/ui now imports only src/app, src/domain, and Fyne. Three violations were fixed: - src/scheduler (NewRealClock): Service.Start() now creates the real clock internally; StartWith(clock) is the injectable seam for tests. - src/platform/desktop (InstallDesktopIntegration): moved into Service.InstallDesktopIcon in new src/app/platform.go. - src/platform/autostart (AutostartStatus, SetAutostart): moved into Service.AutostartStatus and Service.ApplyAutostart in platform.go. go test -race ./... green. Co-Authored-By: Claude Sonnet 4.6 --- docs/REFACTORING.md | 2 +- src/app/operations_test.go | 2 +- src/app/platform.go | 36 ++++++++++++++++++++++++++++++++++++ src/app/service.go | 15 ++++++++++----- src/ui/mainwindow.go | 9 ++------- src/ui/settings_view.go | 7 ++----- 6 files changed, 52 insertions(+), 19 deletions(-) create mode 100644 src/app/platform.go diff --git a/docs/REFACTORING.md b/docs/REFACTORING.md index 1783b1e..b712451 100644 --- a/docs/REFACTORING.md +++ b/docs/REFACTORING.md @@ -274,7 +274,7 @@ Track progress here. Mark tasks complete as they land and pass review. - [x] T4.4 — Extract `history_view.go` - [x] T4.5 — Extract `settings_view.go` - [x] T4.6 — Extract `tray.go`, `singleinstance.go`, `layout.go` -- [ ] T4.7 — Confirm app.go is gone; smoke test both platforms +- [x] T4.7 — Confirm app.go is gone; smoke test both platforms ### Phase 5 — Hardening & docs - [ ] T5.1 — Surface errors from service + storage diff --git a/src/app/operations_test.go b/src/app/operations_test.go index d9345b4..ebfc1db 100644 --- a/src/app/operations_test.go +++ b/src/app/operations_test.go @@ -441,7 +441,7 @@ func TestStartDrivesRunDueOnTick(t *testing.T) { } clock := &appFakeClock{ticks: make(chan time.Time, 1), now: time.Now().Add(2 * time.Minute)} - svc.Start(clock) + svc.StartWith(clock) defer svc.Stop() clock.ticks <- clock.now diff --git a/src/app/platform.go b/src/app/platform.go new file mode 100644 index 0000000..e4680c3 --- /dev/null +++ b/src/app/platform.go @@ -0,0 +1,36 @@ +package app + +import ( + "gitea.mixdep.ru/mix/gosentry/src/platform/autostart" + "gitea.mixdep.ru/mix/gosentry/src/platform/desktop" +) + +// InstallDesktopIcon installs the application's .desktop file and icon on +// Linux (no-op on other platforms). The resulting icon path is stored in +// store.Paths.DesktopIcon so ApplyAutostart can reference it. +func (s *Service) InstallDesktopIcon(appID string, iconBytes []byte) { + if iconPath, err := desktop.InstallDesktopIntegration(appID, s.store.Paths.ExecutablePath, iconBytes); err == nil { + s.store.Paths.DesktopIcon = iconPath + } +} + +// AutostartStatus reports whether the platform autostart entry matches the +// current StartOnLogin setting in the stored config. +func (s *Service) AutostartStatus() (ok bool, message string) { + s.mu.Lock() + enabled := s.store.Config.StartOnLogin + execPath := s.store.Paths.ExecutablePath + s.mu.Unlock() + return autostart.AutostartStatus(enabled, execPath) +} + +// ApplyAutostart writes or removes the platform autostart entry to match the +// current StartOnLogin setting in the stored config. Call after UpdateSettings. +func (s *Service) ApplyAutostart() error { + s.mu.Lock() + enabled := s.store.Config.StartOnLogin + execPath := s.store.Paths.ExecutablePath + iconPath := s.store.Paths.DesktopIcon + s.mu.Unlock() + return autostart.SetAutostart(enabled, execPath, iconPath) +} diff --git a/src/app/service.go b/src/app/service.go index 6ba2797..cb69439 100644 --- a/src/app/service.go +++ b/src/app/service.go @@ -91,11 +91,16 @@ func NewService(store *storage.Store, jobs []domain.Job) *Service { return s } -// Start begins scheduling. It installs a cancelable run context and a timing -// loop driven by the given clock; every tick calls RunDue. Pass -// scheduler.NewRealClock() in production. Start is expected once, during setup, -// before any concurrent use. -func (s *Service) Start(clock scheduler.Clock) { +// Start begins scheduling with the real wall clock. It is the production entry +// point; tests should call StartWith and supply a fake clock instead. Start is +// expected once, during setup, before any concurrent use. +func (s *Service) Start() { + s.StartWith(scheduler.NewRealClock()) +} + +// StartWith begins scheduling driven by the given clock; every tick calls +// RunDue. Used by tests to inject a fake clock. +func (s *Service) StartWith(clock scheduler.Clock) { s.mu.Lock() ctx, cancel := context.WithCancel(context.Background()) s.ctx = ctx diff --git a/src/ui/mainwindow.go b/src/ui/mainwindow.go index 6ed0a18..d69816f 100644 --- a/src/ui/mainwindow.go +++ b/src/ui/mainwindow.go @@ -6,8 +6,6 @@ import ( "gitea.mixdep.ru/mix/gosentry/assets" "gitea.mixdep.ru/mix/gosentry/src/app" "gitea.mixdep.ru/mix/gosentry/src/domain" - "gitea.mixdep.ru/mix/gosentry/src/platform/desktop" - "gitea.mixdep.ru/mix/gosentry/src/scheduler" "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" @@ -26,10 +24,7 @@ func newMainView(w fyne.Window) (fyne.CanvasObject, func(time.Duration, bool)) { if err != nil { return container.NewPadded(widget.NewLabel("Failed to load GoSentry configuration: " + err.Error())), func(time.Duration, bool) {} } - store := svc.Store() - if iconPath, err := desktop.InstallDesktopIntegration(appID, store.Paths.ExecutablePath, assets.IconBytes()); err == nil { - store.Paths.DesktopIcon = iconPath - } + svc.InstallDesktopIcon(appID, assets.IconBytes()) // Build the initial event history from the current runtime state. Jobs and // runtimes are read here only for this one-time initialization; the jobs view @@ -80,7 +75,7 @@ func newMainView(w fyne.Window) (fyne.CanvasObject, func(time.Duration, bool)) { refresh() }) })) - svc.Start(scheduler.NewRealClock()) + svc.Start() tabs := container.NewAppTabs( container.NewTabItemWithIcon("Jobs", theme.ListIcon(), jobsPanel), diff --git a/src/ui/settings_view.go b/src/ui/settings_view.go index b76bf76..5f4e3c5 100644 --- a/src/ui/settings_view.go +++ b/src/ui/settings_view.go @@ -8,7 +8,6 @@ import ( "strings" "gitea.mixdep.ru/mix/gosentry/src/app" - "gitea.mixdep.ru/mix/gosentry/src/platform/autostart" "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" @@ -28,7 +27,7 @@ func settingsView(w fyne.Window, svc *app.Service) fyne.CanvasObject { startOnLogin.SetChecked(store.Config.StartOnLogin) autostartStatus := widget.NewLabel("") refreshAutostartStatus := func() { - ok, message := autostart.AutostartStatus(store.Config.StartOnLogin, store.Paths.ExecutablePath) + ok, message := svc.AutostartStatus() if ok { autostartStatus.SetText("OK: " + message) return @@ -97,9 +96,7 @@ func settingsView(w fyne.Window, svc *app.Service) fyne.CanvasObject { settingsStatus.SetText("Save failed: " + err.Error()) return } - // Autostart is platform integration the Service leaves to the caller (until - // T5.2 introduces an injectable autostart.Manager), so apply it here. - if err := autostart.SetAutostart(store.Config.StartOnLogin, store.Paths.ExecutablePath, store.Paths.DesktopIcon); err != nil { + if err := svc.ApplyAutostart(); err != nil { refreshAutostartStatus() settingsStatus.SetText("Saved, autostart failed: " + err.Error()) return