Release version 0.2.2
Add Linux desktop integration that installs a user-level .desktop file and icon under XDG data directories so taskbars can match the PySentry window to the application icon. Pass the installed icon path into Linux autostart desktop entries when available, while keeping the Windows and fallback autostart APIs compatible. Bump the application version to 0.2.2, update README artifact examples, and record the release notes in docs/CHANGELOG.md. Also adjust the Mermaid architecture diagram so Gitea can render it without invalid SVG line-break tags.
This commit is contained in:
@@ -86,7 +86,7 @@ The binary is written to:
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
# GUI executable produced by scripts\build-windows.bat.
|
# 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:
|
Linux:
|
||||||
@@ -101,7 +101,7 @@ The binary is written to:
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
# Linux executable produced by scripts/build-linux.sh.
|
# 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:
|
Linux using Docker:
|
||||||
@@ -118,7 +118,7 @@ The binary is copied to:
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
# Linux executable copied out of the Docker build image.
|
# 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:
|
Release build from Linux:
|
||||||
@@ -143,13 +143,13 @@ The binaries are copied to:
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
# Linux artifact.
|
# Linux artifact.
|
||||||
dist/linux/pysentry-0.2.1-linux-amd64
|
dist/linux/pysentry-0.2.2-linux-amd64
|
||||||
|
|
||||||
# Linux arm64 artifact.
|
# 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.
|
# 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
|
## Run From Source
|
||||||
@@ -292,7 +292,7 @@ Linux:
|
|||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Type=Application
|
Type=Application
|
||||||
Name=PySentry
|
Name=PySentry
|
||||||
Exec=/opt/pysentry/pysentry-0.2.1-linux-amd64
|
Exec=/opt/pysentry/pysentry-0.2.2-linux-amd64
|
||||||
Terminal=false
|
Terminal=false
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -24,3 +24,7 @@ func Icon() fyne.Resource {
|
|||||||
// resources rather than Fyne runtime state.
|
// resources rather than Fyne runtime state.
|
||||||
return fyne.NewStaticResource("pysentry-icon.png", iconBytes)
|
return fyne.NewStaticResource("pysentry-icon.png", iconBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IconBytes() []byte {
|
||||||
|
return append([]byte(nil), iconBytes...)
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,15 +10,15 @@ job state.
|
|||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
user["Desktop user"]
|
user["Desktop user"]
|
||||||
gui["src/gui\nFyne windows, tabs, dialogs"]
|
gui["src/gui - Fyne windows, tabs, dialogs"]
|
||||||
store["src/core Store\nYAML config and jobs"]
|
store["src/core Store - YAML config and jobs"]
|
||||||
scheduler["src/core Scheduler\n@every and cron timing"]
|
scheduler["src/core Scheduler - @every and cron timing"]
|
||||||
runner["src/core Runner\nshell command execution"]
|
runner["src/core Runner - shell command execution"]
|
||||||
autostart["src/core Autostart\nWindows Run / Linux desktop startup"]
|
autostart["src/core Autostart - Windows Run / Linux desktop startup"]
|
||||||
config["pysentry.yaml\napplication settings"]
|
config["pysentry.yaml - application settings"]
|
||||||
jobs["jobs.yaml\njob definitions"]
|
jobs["jobs.yaml - job definitions"]
|
||||||
logs["logs_dir\nper-run command output logs"]
|
logs["logs_dir - per-run command output logs"]
|
||||||
shell["Platform shell\ncmd.exe /C or sh -c"]
|
shell["Platform shell - cmd.exe /C or sh -c"]
|
||||||
|
|
||||||
user -->|"edits jobs, settings, runs commands"| gui
|
user -->|"edits jobs, settings, runs commands"| gui
|
||||||
gui -->|"OpenStore, SaveConfig, SaveJobs"| store
|
gui -->|"OpenStore, SaveConfig, SaveJobs"| store
|
||||||
|
|||||||
@@ -2,6 +2,15 @@
|
|||||||
|
|
||||||
All notable PySentry changes are recorded in this file.
|
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
|
## 0.2.1 - 2026-06-15
|
||||||
|
|
||||||
- Fixed Docker release scripts so container builds keep Go in `PATH`.
|
- Fixed Docker release scripts so container builds keep Go in `PATH`.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
|
|
||||||
const autostartDesktopFileName = "pysentry.desktop"
|
const autostartDesktopFileName = "pysentry.desktop"
|
||||||
|
|
||||||
func SetAutostart(enabled bool, executablePath string) error {
|
func SetAutostart(enabled bool, executablePath string, iconPath string) error {
|
||||||
desktopPath, err := autostartDesktopPath()
|
desktopPath, err := autostartDesktopPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -31,9 +31,10 @@ Type=Application
|
|||||||
Name=PySentry
|
Name=PySentry
|
||||||
Comment=PySentry desktop scheduler
|
Comment=PySentry desktop scheduler
|
||||||
Exec=%s
|
Exec=%s
|
||||||
|
%s
|
||||||
Terminal=false
|
Terminal=false
|
||||||
X-GNOME-Autostart-enabled=true
|
X-GNOME-Autostart-enabled=true
|
||||||
`, quoteDesktopExec(executablePath))
|
`, quoteDesktopExec(executablePath), desktopIconLine(iconPath))
|
||||||
return os.WriteFile(desktopPath, []byte(desktopFile), 0o644)
|
return os.WriteFile(desktopPath, []byte(desktopFile), 0o644)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,6 +85,13 @@ func quoteDesktopExec(path string) string {
|
|||||||
return strconv.Quote(path)
|
return strconv.Quote(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func desktopIconLine(iconPath string) string {
|
||||||
|
if strings.TrimSpace(iconPath) == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return "Icon=" + iconPath
|
||||||
|
}
|
||||||
|
|
||||||
func cleanupLegacySystemdAutostart() error {
|
func cleanupLegacySystemdAutostart() error {
|
||||||
unitPath, err := legacySystemdUnitPath()
|
unitPath, err := legacySystemdUnitPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ package core
|
|||||||
|
|
||||||
import "fmt"
|
import "fmt"
|
||||||
|
|
||||||
func SetAutostart(enabled bool, executablePath string) error {
|
func SetAutostart(enabled bool, executablePath string, iconPath string) error {
|
||||||
if !enabled {
|
if !enabled {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
const autostartName = "PySentry"
|
const autostartName = "PySentry"
|
||||||
|
|
||||||
func SetAutostart(enabled bool, executablePath string) error {
|
func SetAutostart(enabled bool, executablePath string, iconPath string) error {
|
||||||
if enabled {
|
if enabled {
|
||||||
// Remove any stale entry first. This makes "uncheck, save, check, save"
|
// Remove any stale entry first. This makes "uncheck, save, check, save"
|
||||||
// and even a plain "check, save" repair an old path after the executable
|
// and even a plain "check, save" repair an old path after the executable
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
//go:build !linux
|
||||||
|
|
||||||
|
package core
|
||||||
|
|
||||||
|
func InstallDesktopIntegration(appID string, executablePath string, icon []byte) (string, error) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ type Paths struct {
|
|||||||
JobsDir string
|
JobsDir string
|
||||||
JobsPath string
|
JobsPath string
|
||||||
LogsDir string
|
LogsDir string
|
||||||
|
DesktopIcon string
|
||||||
}
|
}
|
||||||
|
|
||||||
func ResolvePaths() (Paths, error) {
|
func ResolvePaths() (Paths, error) {
|
||||||
|
|||||||
+1
-1
@@ -3,4 +3,4 @@ package core
|
|||||||
// Version is the application version shown in the GUI and used by build
|
// 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
|
// 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.
|
// can override it with Go ldflags when CI tags a build.
|
||||||
var Version = "0.2.1"
|
var Version = "0.2.2"
|
||||||
|
|||||||
+4
-1
@@ -80,6 +80,9 @@ func newMainView(w fyne.Window, started time.Time) fyne.CanvasObject {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return container.NewPadded(widget.NewLabel("Failed to load PySentry configuration: " + err.Error()))
|
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)
|
startupDuration := time.Since(started).Round(time.Millisecond)
|
||||||
events := append([]event{newEvent(0, "Application", "Started", "Startup completed in "+startupDuration.String())}, collectActivity(jobs)...)
|
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())
|
settingsStatus.SetText("Save failed: " + err.Error())
|
||||||
return
|
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()
|
refreshAutostartStatus()
|
||||||
settingsStatus.SetText("Saved, autostart failed: " + err.Error())
|
settingsStatus.SetText("Saved, autostart failed: " + err.Error())
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user