diff --git a/README.md b/README.md index 189ace0..2e97041 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ The binary is written to: ```text # GUI executable produced by scripts\build-windows.bat. -dist\windows\pysentry-0.2.1-windows-amd64.exe +dist\windows\pysentry-0.2.2-windows-amd64.exe ``` Linux: @@ -101,7 +101,7 @@ The binary is written to: ```text # Linux executable produced by scripts/build-linux.sh. -dist/linux/pysentry-0.2.1-linux-amd64 +dist/linux/pysentry-0.2.2-linux-amd64 ``` Linux using Docker: @@ -118,7 +118,7 @@ The binary is copied to: ```text # Linux executable copied out of the Docker build image. -dist\linux\pysentry-0.2.1-linux-amd64 +dist\linux\pysentry-0.2.2-linux-amd64 ``` Release build from Linux: @@ -143,13 +143,13 @@ The binaries are copied to: ```text # Linux artifact. -dist/linux/pysentry-0.2.1-linux-amd64 +dist/linux/pysentry-0.2.2-linux-amd64 # Linux arm64 artifact. -dist/linux/pysentry-0.2.1-linux-arm64 +dist/linux/pysentry-0.2.2-linux-arm64 # Windows artifact cross-compiled from Linux. -dist/windows/pysentry-0.2.1-windows-amd64.exe +dist/windows/pysentry-0.2.2-windows-amd64.exe ``` ## Run From Source @@ -292,7 +292,7 @@ Linux: [Desktop Entry] Type=Application Name=PySentry -Exec=/opt/pysentry/pysentry-0.2.1-linux-amd64 +Exec=/opt/pysentry/pysentry-0.2.2-linux-amd64 Terminal=false ``` diff --git a/assets/assets.go b/assets/assets.go index 5b5dc37..42f2698 100644 --- a/assets/assets.go +++ b/assets/assets.go @@ -24,3 +24,7 @@ func Icon() fyne.Resource { // resources rather than Fyne runtime state. return fyne.NewStaticResource("pysentry-icon.png", iconBytes) } + +func IconBytes() []byte { + return append([]byte(nil), iconBytes...) +} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 4b1573a..5f1538f 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -10,15 +10,15 @@ job state. ```mermaid flowchart LR user["Desktop user"] - gui["src/gui\nFyne windows, tabs, dialogs"] - store["src/core Store\nYAML config and jobs"] - scheduler["src/core Scheduler\n@every and cron timing"] - runner["src/core Runner\nshell command execution"] - autostart["src/core Autostart\nWindows Run / Linux desktop startup"] - config["pysentry.yaml\napplication settings"] - jobs["jobs.yaml\njob definitions"] - logs["logs_dir\nper-run command output logs"] - shell["Platform shell\ncmd.exe /C or sh -c"] + gui["src/gui - Fyne windows, tabs, dialogs"] + store["src/core Store - YAML config and jobs"] + scheduler["src/core Scheduler - @every and cron timing"] + runner["src/core Runner - shell command execution"] + autostart["src/core Autostart - Windows Run / Linux desktop startup"] + config["pysentry.yaml - application settings"] + jobs["jobs.yaml - job definitions"] + logs["logs_dir - per-run command output logs"] + shell["Platform shell - cmd.exe /C or sh -c"] user -->|"edits jobs, settings, runs commands"| gui gui -->|"OpenStore, SaveConfig, SaveJobs"| store diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index bcb586d..7882fd5 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,6 +2,15 @@ All notable PySentry changes are recorded in this file. +## 0.2.2 - 2026-06-15 + +- Added Linux desktop integration that installs a user-level `.desktop` file and icon so taskbars can match the running window to the PySentry icon. +- Added the installed icon path to Linux autostart desktop entries when available. +- Added `ARCHITECTURE.md` with a component interaction diagram and moved project documentation under `docs/`. +- Adjusted the Mermaid architecture diagram to avoid line-break syntax that breaks rendering in Gitea. +- Stabilized the Jobs tab pane layout so switching jobs does not move the divider. +- Added startup timing to the History tab. + ## 0.2.1 - 2026-06-15 - Fixed Docker release scripts so container builds keep Go in `PATH`. diff --git a/src/core/autostart_linux.go b/src/core/autostart_linux.go index 0b26287..36a312c 100644 --- a/src/core/autostart_linux.go +++ b/src/core/autostart_linux.go @@ -13,7 +13,7 @@ import ( const autostartDesktopFileName = "pysentry.desktop" -func SetAutostart(enabled bool, executablePath string) error { +func SetAutostart(enabled bool, executablePath string, iconPath string) error { desktopPath, err := autostartDesktopPath() if err != nil { return err @@ -31,9 +31,10 @@ Type=Application Name=PySentry Comment=PySentry desktop scheduler Exec=%s +%s Terminal=false X-GNOME-Autostart-enabled=true -`, quoteDesktopExec(executablePath)) +`, quoteDesktopExec(executablePath), desktopIconLine(iconPath)) return os.WriteFile(desktopPath, []byte(desktopFile), 0o644) } @@ -84,6 +85,13 @@ func quoteDesktopExec(path string) string { return strconv.Quote(path) } +func desktopIconLine(iconPath string) string { + if strings.TrimSpace(iconPath) == "" { + return "" + } + return "Icon=" + iconPath +} + func cleanupLegacySystemdAutostart() error { unitPath, err := legacySystemdUnitPath() if err != nil { diff --git a/src/core/autostart_other.go b/src/core/autostart_other.go index 5744b91..b9ac296 100644 --- a/src/core/autostart_other.go +++ b/src/core/autostart_other.go @@ -4,7 +4,7 @@ package core import "fmt" -func SetAutostart(enabled bool, executablePath string) error { +func SetAutostart(enabled bool, executablePath string, iconPath string) error { if !enabled { return nil } diff --git a/src/core/autostart_windows.go b/src/core/autostart_windows.go index 6a82dfe..cd2d16f 100644 --- a/src/core/autostart_windows.go +++ b/src/core/autostart_windows.go @@ -9,7 +9,7 @@ import ( const autostartName = "PySentry" -func SetAutostart(enabled bool, executablePath string) error { +func SetAutostart(enabled bool, executablePath string, iconPath string) error { if enabled { // Remove any stale entry first. This makes "uncheck, save, check, save" // and even a plain "check, save" repair an old path after the executable diff --git a/src/core/desktop_linux.go b/src/core/desktop_linux.go new file mode 100644 index 0000000..d619c2d --- /dev/null +++ b/src/core/desktop_linux.go @@ -0,0 +1,60 @@ +//go:build linux + +package core + +import ( + "fmt" + "os" + "path/filepath" +) + +func InstallDesktopIntegration(appID string, executablePath string, icon []byte) (string, error) { + dataHome, err := xdgDataHome() + if err != nil { + return "", err + } + + // The taskbar can only show the application icon reliably when the desktop + // environment can match the window app id to an installed .desktop file and + // icon. Use the user's XDG data directory so portable builds do not need root + // access or a package manager install step. + iconPath := filepath.Join(dataHome, "icons", "hicolor", "256x256", "apps", "pysentry.png") + if err := writeUserFile(iconPath, icon, 0o644); err != nil { + return "", err + } + + desktopPath := filepath.Join(dataHome, "applications", appID+".desktop") + desktopFile := fmt.Sprintf(`[Desktop Entry] +Type=Application +Name=PySentry +Comment=PySentry desktop scheduler +Exec=%s +Icon=%s +Terminal=false +Categories=Utility; +StartupWMClass=%s +`, quoteDesktopExec(executablePath), iconPath, appID) + if err := writeUserFile(desktopPath, []byte(desktopFile), 0o644); err != nil { + return "", err + } + return iconPath, nil +} + +func xdgDataHome() (string, error) { + dataHome := os.Getenv("XDG_DATA_HOME") + if dataHome == "" { + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + dataHome = filepath.Join(home, ".local", "share") + } + return dataHome, nil +} + +func writeUserFile(path string, data []byte, perm os.FileMode) error { + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + return os.WriteFile(path, data, perm) +} diff --git a/src/core/desktop_other.go b/src/core/desktop_other.go new file mode 100644 index 0000000..8ce6ce5 --- /dev/null +++ b/src/core/desktop_other.go @@ -0,0 +1,7 @@ +//go:build !linux + +package core + +func InstallDesktopIntegration(appID string, executablePath string, icon []byte) (string, error) { + return "", nil +} diff --git a/src/core/paths.go b/src/core/paths.go index f5592ff..9304fb5 100644 --- a/src/core/paths.go +++ b/src/core/paths.go @@ -25,6 +25,7 @@ type Paths struct { JobsDir string JobsPath string LogsDir string + DesktopIcon string } func ResolvePaths() (Paths, error) { diff --git a/src/core/version.go b/src/core/version.go index 235df6a..be2dfd6 100644 --- a/src/core/version.go +++ b/src/core/version.go @@ -3,4 +3,4 @@ package core // Version is the application version shown in the GUI and used by build // scripts in artifact names. It is a var rather than a const so release builds // can override it with Go ldflags when CI tags a build. -var Version = "0.2.1" +var Version = "0.2.2" diff --git a/src/gui/app.go b/src/gui/app.go index bfa8969..986acec 100644 --- a/src/gui/app.go +++ b/src/gui/app.go @@ -80,6 +80,9 @@ func newMainView(w fyne.Window, started time.Time) fyne.CanvasObject { if err != nil { return container.NewPadded(widget.NewLabel("Failed to load PySentry configuration: " + err.Error())) } + if iconPath, err := core.InstallDesktopIntegration(appID, store.Paths.ExecutablePath, assets.IconBytes()); err == nil { + store.Paths.DesktopIcon = iconPath + } startupDuration := time.Since(started).Round(time.Millisecond) events := append([]event{newEvent(0, "Application", "Started", "Startup completed in "+startupDuration.String())}, collectActivity(jobs)...) @@ -693,7 +696,7 @@ func settingsView(w fyne.Window, store *core.Store, jobs *[]job) fyne.CanvasObje settingsStatus.SetText("Save failed: " + err.Error()) return } - if err := core.SetAutostart(store.Config.StartOnLogin, store.Paths.ExecutablePath); err != nil { + if err := core.SetAutostart(store.Config.StartOnLogin, store.Paths.ExecutablePath, store.Paths.DesktopIcon); err != nil { refreshAutostartStatus() settingsStatus.SetText("Saved, autostart failed: " + err.Error()) return