Rename project to GoSentry

Rename the application, Go module path, command package, build artifacts, resource script, and embedded icon assets from PySentry/pysentry to GoSentry/gosentry.

Move portable settings to gosentry.yaml while reading legacy pysentry.yaml during the transition, then rewrite settings under the new name.

Update Windows and Linux autostart integration to use GoSentry names while cleaning up legacy PySentry registry, desktop-entry, and systemd artifacts.

Refresh README, architecture notes, roadmap, changelog, and release examples for version 0.3.0.
This commit is contained in:
mixeme
2026-06-17 07:29:58 +03:00
parent d828e34121
commit 94033e794f
28 changed files with 182 additions and 112 deletions
+1
View File
@@ -2,6 +2,7 @@
bin
dist
logs
gosentry.yaml
pysentry.yaml
jobs.yaml
*.exe
+3 -2
View File
@@ -1,14 +1,15 @@
# Build outputs
dist/
# Generated Windows resource compiled from packaging/windows/pysentry.rc.
cmd/pysentry/*.syso
# Generated Windows resource compiled from packaging/windows/gosentry.rc.
cmd/gosentry/*.syso
# Local binaries that may be produced by ad-hoc go build commands.
*.exe
*.test
# Runtime files created next to the executable during local runs.
gosentry.yaml
pysentry.yaml
jobs.yaml
logs/
+31 -31
View File
@@ -1,8 +1,8 @@
# PySentry
# GoSentry
PySentry is a cross-platform desktop scheduler inspired by cron. It provides a native GUI for creating, grouping, pausing, running, and monitoring scheduled shell commands.
GoSentry is a cross-platform desktop scheduler inspired by cron. It provides a native GUI for creating, grouping, pausing, running, and monitoring scheduled shell commands.
PySentry is being designed and implemented with assistance from OpenAI Codex.
GoSentry is being designed and implemented with assistance from OpenAI Codex.
Project notes:
@@ -72,7 +72,7 @@ sudo apt install golang gcc libgl1-mesa-dev xorg-dev
Windows:
```powershell
# Builds dist\windows\pysentry-<version>-windows-amd64.exe. The script changes
# Builds dist\windows\gosentry-<version>-windows-amd64.exe. The script changes
# to the repository root first, so double-clicking it from Explorer works. It
# also adds MSYS2 UCRT64 to PATH for this process only, embeds the Windows icon
# when windres is available, and uses the Windows GUI subsystem so no console
@@ -86,7 +86,7 @@ The binary is written to:
```text
# GUI executable produced by scripts\build-windows.bat.
dist\windows\pysentry-0.2.5-windows-amd64.exe
dist\windows\gosentry-0.3.0-windows-amd64.exe
```
Linux:
@@ -101,14 +101,14 @@ The binary is written to:
```text
# Linux executable produced by scripts/build-linux.sh.
dist/linux/pysentry-0.2.5-linux-amd64
dist/linux/gosentry-0.3.0-linux-amd64
```
Linux using Docker:
```bash
# Builds the Linux binary inside Docker using the versioned image tag
# gitea.mixdep.ru/mix/pysentry-builder:<version>. Useful from hosts or CI jobs
# gitea.mixdep.ru/mix/gosentry-builder:<version>. Useful from hosts or CI jobs
# where the native Linux/Fyne packages are not installed locally.
chmod +x ./scripts/build-linux-docker.sh
./scripts/build-linux-docker.sh
@@ -118,7 +118,7 @@ The binary is copied to:
```text
# Linux executable copied out of the Docker build image.
dist\linux\pysentry-0.2.5-linux-amd64
dist\linux\gosentry-0.3.0-linux-amd64
```
Release build from Linux:
@@ -143,13 +143,13 @@ The binaries are copied to:
```text
# Linux artifact.
dist/linux/pysentry-0.2.5-linux-amd64
dist/linux/gosentry-0.3.0-linux-amd64
# Linux arm64 artifact.
dist/linux/pysentry-0.2.5-linux-arm64
dist/linux/gosentry-0.3.0-linux-arm64
# Windows artifact cross-compiled from Linux.
dist/windows/pysentry-0.2.5-windows-amd64.exe
dist/windows/gosentry-0.3.0-windows-amd64.exe
```
## Run From Source
@@ -164,7 +164,7 @@ $env:CGO_ENABLED = '1'
# go run starts the app from source. Use scripts\build-windows.bat when you need
# a standalone .exe without a console window.
& 'C:\Program Files\Go\bin\go.exe' run ./cmd/pysentry
& 'C:\Program Files\Go\bin\go.exe' run ./cmd/gosentry
```
Linux:
@@ -172,14 +172,14 @@ Linux:
```bash
# CGO must stay enabled because the Fyne GUI links against native Linux desktop
# libraries.
CGO_ENABLED=1 go run ./cmd/pysentry
CGO_ENABLED=1 go run ./cmd/gosentry
```
## Troubleshooting
### Windows, VirtualBox, RDP, And OpenGL
PySentry uses [Fyne](https://fyne.io/), and Fyne uses GLFW/OpenGL to create the
GoSentry uses [Fyne](https://fyne.io/), and Fyne uses GLFW/OpenGL to create the
desktop window. In a Windows virtual machine, especially when the session is
opened through RDP inside VirtualBox, the available video driver can fail OpenGL
initialization.
@@ -202,11 +202,11 @@ Known workaround:
checksum files are not needed for this workaround.
2. Open the downloaded archive and use the `x64` build from it.
3. Copy the Mesa OpenGL DLL files from `x64` into the same directory as the
PySentry `.exe`, for example:
GoSentry `.exe`, for example:
```text
dist\windows\
pysentry-0.2.5-windows-amd64.exe
gosentry-0.3.0-windows-amd64.exe
opengl32.dll
...
```
@@ -217,12 +217,12 @@ driver does not provide usable OpenGL.
## Storage
PySentry creates its runtime files next to the executable by default.
GoSentry creates its runtime files next to the executable by default.
`pysentry.yaml` stores application settings:
`gosentry.yaml` stores application settings:
```yaml
# Directory containing jobs.yaml. "." means "the folder where the PySentry
# Directory containing jobs.yaml. "." means "the folder where the GoSentry
# executable lives"; an absolute path can be used when jobs should live elsewhere.
jobs_dir: .
@@ -236,7 +236,7 @@ max_log_files: 100
# Delete .log files older than this many days during cleanup.
max_log_age_days: 30
# Start PySentry automatically when the current desktop user signs in.
# Start GoSentry automatically when the current desktop user signs in.
start_on_login: false
# Closing the window hides it to the tray instead of stopping the scheduler.
@@ -267,7 +267,7 @@ jobs:
schedule: '@every 1m'
# Command passed to the platform shell: cmd.exe /C on Windows, sh -c on Linux.
command: echo PySentry test job: scheduler is alive
command: echo GoSentry test job: scheduler is alive
# Disabled jobs remain in jobs.yaml but are skipped by the scheduler.
enabled: true
@@ -302,7 +302,7 @@ Standard 5-field cron schedules:
## Using The App
1. Start PySentry.
1. Start GoSentry.
2. Use `New job` to create a command.
3. Set `Schedule`, `Command`, optional `Folder`, and `Enabled`.
4. Use `Run now` for a manual test run.
@@ -318,29 +318,29 @@ Autostart entries add `--start-in-tray`, so scheduled jobs begin running after s
## Autostart
PySentry is a user desktop application, not a system daemon, so autostart should be configured per user.
GoSentry is a user desktop application, not a system daemon, so autostart should be configured per user.
Linux:
```ini
# PySentry writes an XDG Autostart desktop entry when Start on login is enabled.
# GoSentry writes an XDG Autostart desktop entry when Start on login is enabled.
# This is better for a GUI/tray application than a systemd user service because
# the desktop environment starts it inside the graphical user session.
# Saving the setting also removes the old ~/.config/systemd/user/pysentry.service
# unit if it was created by an earlier PySentry build.
~/.config/autostart/pysentry.desktop
# unit if it was created by an earlier GoSentry build.
~/.config/autostart/gosentry.desktop
[Desktop Entry]
Type=Application
Name=PySentry
Exec=/opt/pysentry/pysentry-0.2.5-linux-amd64 --start-in-tray
Name=GoSentry
Exec=/opt/gosentry/gosentry-0.3.0-linux-amd64 --start-in-tray
Terminal=false
```
Windows:
```text
# PySentry writes a shortcut to the current user's Startup folder when Start on
# GoSentry writes a shortcut to the current user's Startup folder when Start on
# login is enabled. A .lnk stores the executable path as a structured TargetPath,
# and stores --start-in-tray as Arguments, so paths with spaces do not need
# fragile command-line quoting. Saving settings rewrites the shortcut and removes
@@ -350,7 +350,7 @@ Windows:
## Project Layout
- `cmd/pysentry` starts the desktop app.
- `cmd/gosentry` starts the desktop app.
- `src/gui` contains the GUI.
- `src/core` contains YAML storage, command execution, scheduling, and log cleanup.
- `assets` contains app icons that are embedded into the application binary.
@@ -361,7 +361,7 @@ Build outputs are written to `dist/`. The old local `bin/` directory is not used
## Dependencies
PySentry keeps the direct dependency list intentionally small:
GoSentry keeps the direct dependency list intentionally small:
- [`fyne.io/fyne/v2`](https://fyne.io/) for the native GUI.
- `github.com/robfig/cron/v3` for cron schedule parsing.
+2 -2
View File
@@ -14,7 +14,7 @@ import (
// The blank import enables the compiler directive below; no runtime package
// initialization from embed is required.
//
//go:embed pysentry-icon-big.png
//go:embed gosentry-icon-big.png
var iconBytes []byte
func Icon() fyne.Resource {
@@ -22,7 +22,7 @@ func Icon() fyne.Resource {
// for the window icon and tray icon. The Windows Explorer icon is still added
// by the build script through the .ico resource, because Explorer reads PE
// resources rather than Fyne runtime state.
return fyne.NewStaticResource("pysentry-icon-big.png", iconBytes)
return fyne.NewStaticResource("gosentry-icon-big.png", iconBytes)
}
func IconBytes() []byte {

Before

Width:  |  Height:  |  Size: 299 B

After

Width:  |  Height:  |  Size: 299 B

Before

Width:  |  Height:  |  Size: 996 KiB

After

Width:  |  Height:  |  Size: 996 KiB

@@ -3,8 +3,8 @@ package main
import (
"os"
"github.com/pysentry/pysentry/src/core"
"github.com/pysentry/pysentry/src/gui"
"gitea.mixdep.ru/mix/gosentry/src/core"
"gitea.mixdep.ru/mix/gosentry/src/gui"
)
func main() {
+5 -5
View File
@@ -1,6 +1,6 @@
# PySentry Architecture
# GoSentry Architecture
This document shows the current component interaction model. PySentry is still a
This document shows the current component interaction model. GoSentry is still a
single desktop process: the GUI, scheduler, storage, and command runner live in
one application and communicate through Go function calls and shared in-memory
job state.
@@ -15,7 +15,7 @@ flowchart LR
scheduler["src/core Scheduler - @every and cron timing"]
runner["src/core Runner - shell command execution"]
autostart["src/core Autostart - Windows Startup shortcut / Linux desktop startup"]
config["pysentry.yaml - application settings"]
config["gosentry.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"]
@@ -41,8 +41,8 @@ flowchart LR
## Main Flows
1. Startup:
The executable starts `cmd/pysentry`, which calls the GUI package. The GUI
opens the store, loads `pysentry.yaml` and `jobs.yaml`, creates the main tabs,
The executable starts `cmd/gosentry`, which calls the GUI package. The GUI
opens the store, loads `gosentry.yaml` and `jobs.yaml`, creates the main tabs,
then starts the scheduler with the loaded job slice.
2. Editing settings or jobs:
+10 -2
View File
@@ -1,6 +1,14 @@
# Changelog
All notable PySentry changes are recorded in this file.
All notable GoSentry changes are recorded in this file.
## 0.3.0 - 2026-06-17
- Renamed the project from PySentry to GoSentry across the GUI, module path, build scripts, generated artifacts, desktop integration, and documentation.
- Renamed the command package to `cmd/gosentry` and Windows resource script to `packaging/windows/gosentry.rc`.
- Renamed portable application settings from `pysentry.yaml` to `gosentry.yaml`, while keeping one-time read compatibility for existing `pysentry.yaml` files.
- Renamed build artifacts from `pysentry-*` to `gosentry-*`.
- Updated autostart and Linux desktop integration to use GoSentry names while cleaning up older PySentry autostart entries.
## 0.2.5 - 2026-06-16
@@ -27,7 +35,7 @@ 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 Linux desktop integration that installs a user-level `.desktop` file and icon so taskbars can match the running window to the GoSentry 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.
+4 -20
View File
@@ -1,22 +1,6 @@
# Roadmap
This file tracks planned PySentry work that is larger than a single bug fix.
## Project Rename
Plan a rename from PySentry to GoSentry.
Rename checklist:
- Decide the final repository path and Go module path.
- Update application name, window title, tray menu, and desktop integration text.
- Update app ID and autostart entry names.
- Rename build artifacts from `pysentry-*` to `gosentry-*`.
- Decide whether runtime files should stay backward-compatible with existing
`pysentry.yaml`, `jobs.yaml`, and log directories or migrate to new names.
- Update README, CHANGELOG, ROADMAP, build scripts, Docker image names, and
package metadata.
- Consider a transition note for users with existing PySentry configuration.
This file tracks planned GoSentry work that is larger than a single bug fix.
## Post-Field-Test Cleanup
@@ -56,7 +40,7 @@ runtime YAML files live next to the executable by default.
Planned delivery variants:
- Windows portable `.zip` with `pysentry.exe`, `README.md`, and `CHANGELOG.md`.
- Windows portable `.zip` with `gosentry.exe`, `README.md`, and `CHANGELOG.md`.
- Linux portable `.tar.gz` archives for `linux-amd64` and `linux-arm64`.
- Debian/Ubuntu `.deb` package once the Linux runtime paths are settled.
- Windows installer later, likely Inno Setup first and MSI/WiX only if needed.
@@ -68,8 +52,8 @@ Packaging design note:
- Portable builds can keep settings and jobs next to the executable.
- Installer/package builds should move runtime data to per-user locations:
`%APPDATA%\PySentry` on Windows, and XDG directories such as
`~/.config/pysentry` and `~/.local/share/pysentry` on Linux.
`%APPDATA%\GoSentry` on Windows, and XDG directories such as
`~/.config/gosentry` and `~/.local/share/gosentry` on Linux.
Initial priority:
+1 -1
View File
@@ -1,4 +1,4 @@
module github.com/pysentry/pysentry
module gitea.mixdep.ru/mix/gosentry
go 1.22
+3 -3
View File
@@ -6,8 +6,8 @@ set -euo pipefail
# default includes the application version and target platform.
version="$(sed -n 's/^var Version = "\(.*\)"/\1/p' src/core/version.go)"
version="${version:-0.0.0-dev}"
tag="gitea.mixdep.ru/mix/pysentry-builder:${version}"
output="${1:-dist/linux/pysentry-${version}-linux-amd64}"
tag="gitea.mixdep.ru/mix/gosentry-builder:${version}"
output="${1:-dist/linux/gosentry-${version}-linux-amd64}"
docker_user_args=()
if command -v id >/dev/null 2>&1; then
docker_user_args=(--user "$(id -u):$(id -g)")
@@ -26,7 +26,7 @@ docker run --rm \
-v "$(pwd):/src" \
-w /src \
"$tag" \
bash -c 'CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -buildvcs=false -trimpath -ldflags "-s -w -X github.com/pysentry/pysentry/src/core.Version=${VERSION}" -o "${OUTPUT}" ./cmd/pysentry'
bash -c 'CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -buildvcs=false -trimpath -ldflags "-s -w -X gitea.mixdep.ru/mix/gosentry/src/core.Version=${VERSION}" -o "${OUTPUT}" ./cmd/gosentry'
# Icons are embedded in the Go binary, so there is no assets directory to copy
# after extracting the Linux executable.
+2 -2
View File
@@ -5,7 +5,7 @@ set -euo pipefail
# default includes the application version and target platform.
version="$(sed -n 's/^var Version = "\(.*\)"/\1/p' src/core/version.go)"
version="${version:-0.0.0-dev}"
output="${1:-dist/linux/pysentry-${version}-linux-amd64}"
output="${1:-dist/linux/gosentry-${version}-linux-amd64}"
mkdir -p "$(dirname "$output")"
# Fyne needs CGO for its native desktop backend. The script pins the target to
@@ -17,7 +17,7 @@ export GOARCH=amd64
# -trimpath removes local machine paths from debug/build metadata. -s -w strips
# symbol/debug tables to keep the desktop binary smaller.
go build -trimpath -ldflags "-s -w -X github.com/pysentry/pysentry/src/core.Version=${version}" -o "$output" ./cmd/pysentry
go build -trimpath -ldflags "-s -w -X gitea.mixdep.ru/mix/gosentry/src/core.Version=${version}" -o "$output" ./cmd/gosentry
# The application icon is embedded by Go, so the Linux build does not need a
# sidecar assets directory beside the executable.
+7 -7
View File
@@ -11,7 +11,7 @@ cd "$repo_root"
version="$(sed -n 's/^var Version = "\(.*\)"/\1/p' src/core/version.go)"
version="${version:-0.0.0-dev}"
tag="gitea.mixdep.ru/mix/pysentry-builder:${version}"
tag="gitea.mixdep.ru/mix/gosentry-builder:${version}"
docker_user_args=()
if command -v id >/dev/null 2>&1; then
@@ -24,9 +24,9 @@ Usage: $0 [target...]
Targets:
all Build every release artifact.
linux-amd64 Build dist/linux/pysentry-${version}-linux-amd64.
linux-arm64 Build dist/linux/pysentry-${version}-linux-arm64.
windows-amd64 Build dist/windows/pysentry-${version}-windows-amd64.exe.
linux-amd64 Build dist/linux/gosentry-${version}-linux-amd64.
linux-arm64 Build dist/linux/gosentry-${version}-linux-arm64.
windows-amd64 Build dist/windows/gosentry-${version}-windows-amd64.exe.
When no target is passed and the script runs in a terminal, it asks what to build.
EOF
@@ -99,15 +99,15 @@ run_in_builder() {
}
build_linux_amd64() {
run_in_builder 'mkdir -p dist/linux && CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -buildvcs=false -trimpath -ldflags "-s -w -X github.com/pysentry/pysentry/src/core.Version=${VERSION}" -o "dist/linux/pysentry-${VERSION}-linux-amd64" ./cmd/pysentry'
run_in_builder 'mkdir -p dist/linux && CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -buildvcs=false -trimpath -ldflags "-s -w -X gitea.mixdep.ru/mix/gosentry/src/core.Version=${VERSION}" -o "dist/linux/gosentry-${VERSION}-linux-amd64" ./cmd/gosentry'
}
build_linux_arm64() {
run_in_builder 'mkdir -p dist/linux && CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOOS=linux GOARCH=arm64 CGO_CFLAGS="--sysroot=/ -I/usr/include/aarch64-linux-gnu" CGO_LDFLAGS="--sysroot=/ -L/usr/lib/aarch64-linux-gnu" PKG_CONFIG_LIBDIR=/usr/lib/aarch64-linux-gnu/pkgconfig go build -buildvcs=false -trimpath -ldflags "-s -w -X github.com/pysentry/pysentry/src/core.Version=${VERSION}" -o "dist/linux/pysentry-${VERSION}-linux-arm64" ./cmd/pysentry'
run_in_builder 'mkdir -p dist/linux && CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOOS=linux GOARCH=arm64 CGO_CFLAGS="--sysroot=/ -I/usr/include/aarch64-linux-gnu" CGO_LDFLAGS="--sysroot=/ -L/usr/lib/aarch64-linux-gnu" PKG_CONFIG_LIBDIR=/usr/lib/aarch64-linux-gnu/pkgconfig go build -buildvcs=false -trimpath -ldflags "-s -w -X gitea.mixdep.ru/mix/gosentry/src/core.Version=${VERSION}" -o "dist/linux/gosentry-${VERSION}-linux-arm64" ./cmd/gosentry'
}
build_windows_amd64() {
run_in_builder 'mkdir -p dist/windows && x86_64-w64-mingw32-windres -O coff -o cmd/pysentry/rsrc_windows_amd64.syso packaging/windows/pysentry.rc && CC=x86_64-w64-mingw32-gcc CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -buildvcs=false -trimpath -ldflags "-s -w -H=windowsgui -X github.com/pysentry/pysentry/src/core.Version=${VERSION}" -o "dist/windows/pysentry-${VERSION}-windows-amd64.exe" ./cmd/pysentry'
run_in_builder 'mkdir -p dist/windows && x86_64-w64-mingw32-windres -O coff -o cmd/gosentry/rsrc_windows_amd64.syso packaging/windows/gosentry.rc && CC=x86_64-w64-mingw32-gcc CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -buildvcs=false -trimpath -ldflags "-s -w -H=windowsgui -X gitea.mixdep.ru/mix/gosentry/src/core.Version=${VERSION}" -o "dist/windows/gosentry-${VERSION}-windows-amd64.exe" ./cmd/gosentry'
}
mapfile -t targets < <(choose_targets "$@" | normalize_targets | awk '!seen[$0]++')
+5 -5
View File
@@ -3,7 +3,7 @@ setlocal enabledelayedexpansion
REM Double-clicking a .bat file can start it with an arbitrary working
REM directory. Move to the repository root (the parent of scripts\) before using
REM relative paths such as .\cmd\pysentry and packaging\windows\pysentry.rc.
REM relative paths such as .\cmd\gosentry and packaging\windows\gosentry.rc.
cd /d "%~dp0\.."
for /f "tokens=4" %%V in ('findstr /C:"var Version" src\core\version.go') do set "VERSION=%%~V"
@@ -14,7 +14,7 @@ REM Optional first argument allows CI or a developer to choose another output
REM path. The default keeps all generated binaries under dist\ so the source tree
REM stays clean and the old bin\ folder is no longer needed.
set "OUTPUT=%~1"
if "%OUTPUT%"=="" set "OUTPUT=dist\windows\pysentry-%VERSION%-windows-amd64.exe"
if "%OUTPUT%"=="" set "OUTPUT=dist\windows\gosentry-%VERSION%-windows-amd64.exe"
REM Prefer the standard Go installer path on Windows, but fall back to PATH for
REM machines where Go was installed by another package manager.
@@ -37,17 +37,17 @@ for %%I in ("%OUTPUT%") do set "OUTDIR=%%~dpI"
if not exist "%OUTDIR%" mkdir "%OUTDIR%"
REM windres embeds the .ico file into the PE executable so Windows Explorer,
REM shortcuts, and the taskbar can show the PySentry icon. The Go embed package
REM shortcuts, and the taskbar can show the GoSentry icon. The Go embed package
REM handles Fyne's runtime icon, but Explorer reads this Windows resource instead.
where windres.exe >nul 2>nul
if %ERRORLEVEL%==0 (
windres.exe -O coff -o cmd\pysentry\rsrc_windows_amd64.syso packaging\windows\pysentry.rc
windres.exe -O coff -o cmd\gosentry\rsrc_windows_amd64.syso packaging\windows\gosentry.rc
)
REM -trimpath removes local machine paths from the binary, -s -w reduce binary
REM size, and -H=windowsgui prevents a separate console window from opening when
REM the GUI app starts from Explorer or a shortcut.
"%GOEXE%" build -trimpath -ldflags "-s -w -H=windowsgui -X github.com/pysentry/pysentry/src/core.Version=%VERSION%" -o "%OUTPUT%" .\cmd\pysentry
"%GOEXE%" build -trimpath -ldflags "-s -w -H=windowsgui -X gitea.mixdep.ru/mix/gosentry/src/core.Version=%VERSION%" -o "%OUTPUT%" .\cmd\gosentry
if errorlevel 1 exit /b 1
REM Icons are embedded into the executable, so no assets directory is copied next
+43 -4
View File
@@ -11,7 +11,8 @@ import (
"strings"
)
const autostartDesktopFileName = "pysentry.desktop"
const autostartDesktopFileName = "gosentry.desktop"
const legacyAutostartDesktopFileName = "pysentry.desktop"
func SetAutostart(enabled bool, executablePath string, iconPath string) error {
desktopPath, err := autostartDesktopPath()
@@ -21,6 +22,9 @@ func SetAutostart(enabled bool, executablePath string, iconPath string) error {
if err := cleanupLegacySystemdAutostart(); err != nil {
return err
}
if err := cleanupLegacyDesktopAutostart(); err != nil {
return err
}
if enabled {
if err := os.MkdirAll(filepath.Dir(desktopPath), 0o755); err != nil {
@@ -28,8 +32,8 @@ func SetAutostart(enabled bool, executablePath string, iconPath string) error {
}
desktopFile := fmt.Sprintf(`[Desktop Entry]
Type=Application
Name=PySentry
Comment=PySentry desktop scheduler
Name=GoSentry
Comment=GoSentry desktop scheduler
Exec=%s %s
%s
Terminal=false
@@ -52,6 +56,9 @@ func AutostartStatus(expectedEnabled bool, executablePath string) (bool, string)
if legacySystemdAutostartExists() {
return false, "Legacy systemd autostart entry still exists"
}
if legacyDesktopAutostartExists() {
return false, "Legacy desktop autostart entry still exists"
}
data, readErr := os.ReadFile(desktopPath)
if !expectedEnabled {
@@ -82,6 +89,18 @@ func autostartDesktopPath() (string, error) {
return filepath.Join(configHome, "autostart", autostartDesktopFileName), nil
}
func legacyAutostartDesktopPath() (string, error) {
configHome := os.Getenv("XDG_CONFIG_HOME")
if configHome == "" {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
configHome = filepath.Join(home, ".config")
}
return filepath.Join(configHome, "autostart", legacyAutostartDesktopFileName), nil
}
func quoteDesktopExec(path string) string {
return strconv.Quote(path)
}
@@ -103,7 +122,7 @@ func cleanupLegacySystemdAutostart() error {
}
// Older PySentry builds used a systemd user unit for autostart. The current
// Linux implementation uses XDG Autostart because PySentry is a GUI/tray
// GoSentry implementation uses XDG Autostart because it is a GUI/tray
// application and should be launched by the desktop session. Disable and
// remove the old unit so the two mechanisms do not fight or start duplicates.
_ = exec.Command("systemctl", "--user", "disable", "pysentry.service").Run()
@@ -114,6 +133,26 @@ func cleanupLegacySystemdAutostart() error {
return nil
}
func cleanupLegacyDesktopAutostart() error {
desktopPath, err := legacyAutostartDesktopPath()
if err != nil {
return err
}
if err := os.Remove(desktopPath); err != nil && !os.IsNotExist(err) {
return err
}
return nil
}
func legacyDesktopAutostartExists() bool {
desktopPath, err := legacyAutostartDesktopPath()
if err != nil {
return false
}
_, err = os.Stat(desktopPath)
return err == nil
}
func legacySystemdAutostartExists() bool {
unitPath, err := legacySystemdUnitPath()
if err != nil {
+23
View File
@@ -4,6 +4,7 @@ package core
import (
"os"
"path/filepath"
"strings"
"testing"
)
@@ -30,3 +31,25 @@ func TestLinuxAutostartStartsInTray(t *testing.T) {
t.Fatalf("desktop entry does not start in tray: %s", data)
}
}
func TestLinuxAutostartRemovesLegacyDesktopEntry(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
legacyPath, err := legacyAutostartDesktopPath()
if err != nil {
t.Fatalf("resolve legacy desktop path: %v", err)
}
if err := os.MkdirAll(filepath.Dir(legacyPath), 0o755); err != nil {
t.Fatalf("create legacy desktop directory: %v", err)
}
if err := os.WriteFile(legacyPath, []byte("[Desktop Entry]\nName=PySentry\n"), 0o644); err != nil {
t.Fatalf("write legacy desktop entry: %v", err)
}
if err := SetAutostart(true, "/opt/gosentry/gosentry", ""); err != nil {
t.Fatalf("enable autostart: %v", err)
}
if _, err := os.Stat(legacyPath); !os.IsNotExist(err) {
t.Fatalf("legacy desktop entry still exists or cannot be checked: %v", err)
}
}
+3 -3
View File
@@ -11,19 +11,19 @@ import (
func TestParseRegistryRunValue(t *testing.T) {
output := `
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run
PySentry REG_SZ "D:\Apps\PySentry\pysentry.exe"
GoSentry REG_SZ "D:\Apps\GoSentry\gosentry.exe"
`
value, ok := parseRegistryRunValue(output)
if !ok {
t.Fatal("expected registry value to parse")
}
if value != `D:\Apps\PySentry\pysentry.exe` {
if value != `D:\Apps\GoSentry\gosentry.exe` {
t.Fatalf("unexpected value: %q", value)
}
}
func TestSameWindowsPathIgnoresCaseAndQuotes(t *testing.T) {
if !sameWindowsPath(`"D:\Apps\PySentry\pysentry.exe"`, `d:\apps\pysentry\pysentry.exe`) {
if !sameWindowsPath(`"D:\Apps\GoSentry\gosentry.exe"`, `d:\apps\gosentry\gosentry.exe`) {
t.Fatal("expected paths to match")
}
}
+3 -3
View File
@@ -18,7 +18,7 @@ func InstallDesktopIntegration(appID string, executablePath string, icon []byte)
// 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")
iconPath := filepath.Join(dataHome, "icons", "hicolor", "256x256", "apps", "gosentry.png")
if err := writeUserFile(iconPath, icon, 0o644); err != nil {
return "", err
}
@@ -26,8 +26,8 @@ func InstallDesktopIntegration(appID string, executablePath string, icon []byte)
desktopPath := filepath.Join(dataHome, "applications", appID+".desktop")
desktopFile := fmt.Sprintf(`[Desktop Entry]
Type=Application
Name=PySentry
Comment=PySentry desktop scheduler
Name=GoSentry
Comment=GoSentry desktop scheduler
Exec=%s
Icon=%s
Terminal=false
+2 -2
View File
@@ -7,7 +7,7 @@ import "time"
// launches omit this flag and open the normal window.
const StartInTrayArgument = "--start-in-tray"
// Config is stored in pysentry.yaml next to the program. It contains only
// Config is stored in gosentry.yaml next to the program. It contains only
// application-level choices: where to read jobs from, where to write logs, and
// how the desktop shell should behave.
type Config struct {
@@ -29,7 +29,7 @@ type JobsFile struct {
// Job is the user-visible scheduled command.
//
// Fields with yaml:"-" are deliberately runtime-only. They are useful in the GUI
// while PySentry is running, but writing them to jobs.yaml would make the jobs
// while GoSentry is running, but writing them to jobs.yaml would make the jobs
// file noisy and would mix durable configuration with transient execution state.
type Job struct {
ID int `yaml:"id"`
+5 -1
View File
@@ -8,7 +8,11 @@ import (
const (
// The config file stays beside the executable so the portable build behaves
// predictably: moving the program folder moves its settings with it.
ConfigFileName = "pysentry.yaml"
ConfigFileName = "gosentry.yaml"
// Older builds were named PySentry. Keep the old config name readable during
// the rename window so portable installations can start once and rewrite the
// settings to gosentry.yaml without manual file copying.
LegacyConfigFileName = "pysentry.yaml"
// Jobs are kept in a separate YAML file because the user can choose a
// different jobs directory, while application settings remain local to the
// installed/copied program.
+1 -1
View File
@@ -94,7 +94,7 @@ func CleanupLogs(logsDir string, maxFiles int, maxAgeDays int) error {
var logs []logFile
cutoff := time.Now().AddDate(0, 0, -maxAgeDays)
for _, entry := range entries {
// Only PySentry run logs are managed here. Directories and non-.log files
// Only GoSentry run logs are managed here. Directories and non-.log files
// are intentionally ignored so the user can keep notes or other artifacts
// in the same folder without the cleanup policy deleting them.
if entry.IsDir() || !strings.HasSuffix(strings.ToLower(entry.Name()), ".log") {
+1 -1
View File
@@ -6,7 +6,7 @@ import (
)
func configureHiddenWindow(command *exec.Cmd) {
// PySentry is a GUI scheduler, so child commands should not flash a console
// GoSentry is a GUI scheduler, so child commands should not flash a console
// window on Windows. CREATE_NO_WINDOW keeps cmd.exe and simple console tools
// quiet while stdout/stderr are still captured through pipes.
command.SysProcAttr = &syscall.SysProcAttr{
+1 -1
View File
@@ -222,7 +222,7 @@ func nextRunTime(schedule string, from time.Time) (time.Time, bool) {
}
return from.Add(interval), true
}
// Standard five-field cron keeps PySentry compatible with the mental model
// Standard five-field cron keeps GoSentry compatible with the mental model
// users already know from Unix cron, while robfig/cron handles edge cases
// such as ranges, steps, and day-of-week names.
parsed, err := cronParser.Parse(schedule)
+15 -5
View File
@@ -77,11 +77,21 @@ func loadOrCreateConfig(paths Paths) (Config, error) {
NotifyOnFailure: true,
}
if _, err := os.Stat(paths.ConfigPath); errors.Is(err, os.ErrNotExist) {
configPath := paths.ConfigPath
if _, err := os.Stat(configPath); errors.Is(err, os.ErrNotExist) {
legacyPath := filepath.Join(paths.AppDir, LegacyConfigFileName)
if _, legacyErr := os.Stat(legacyPath); legacyErr == nil {
configPath = legacyPath
} else {
return config, writeYAML(paths.ConfigPath, config)
}
}
if _, err := os.Stat(configPath); errors.Is(err, os.ErrNotExist) {
return config, writeYAML(paths.ConfigPath, config)
}
data, err := os.ReadFile(paths.ConfigPath)
data, err := os.ReadFile(configPath)
if err != nil {
return Config{}, err
}
@@ -146,7 +156,7 @@ func normalizeJobs(jobs []Job) {
if strings.TrimSpace(job.Command) == "" {
// An empty command would fail in a confusing way. A safe echo command
// gives the user something observable and harmless instead.
job.Command = echoCommand("PySentry job ran")
job.Command = echoCommand("GoSentry job ran")
}
if job.LastRun == "" {
job.LastRun = "Never"
@@ -209,7 +219,7 @@ func defaultJobs() []Job {
Name: "Hello scheduler",
Folder: "Examples",
Schedule: "@every 1m",
Command: echoCommand("PySentry test job: scheduler is alive"),
Command: echoCommand("GoSentry test job: scheduler is alive"),
Enabled: true,
},
{
@@ -217,7 +227,7 @@ func defaultJobs() []Job {
Name: "Write timestamp",
Folder: "Examples",
Schedule: "*/1 * * * *",
Command: echoCommand("PySentry test job: timestamp command ran"),
Command: echoCommand("GoSentry test job: timestamp command ran"),
Enabled: true,
},
{
+1 -1
View File
@@ -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.5"
var Version = "0.3.0"
+8 -8
View File
@@ -12,8 +12,8 @@ import (
"strings"
"time"
"github.com/pysentry/pysentry/assets"
"github.com/pysentry/pysentry/src/core"
"gitea.mixdep.ru/mix/gosentry/assets"
"gitea.mixdep.ru/mix/gosentry/src/core"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
@@ -25,7 +25,7 @@ import (
"fyne.io/fyne/v2/widget"
)
const appID = "io.github.pysentry.desktop"
const appID = "ru.mixdep.gosentry.desktop"
const allFolders = "All"
const noFolder = "No folder"
const minJobsSidebarWidth float32 = 480
@@ -57,7 +57,7 @@ func Run(startInTray bool) {
a := app.NewWithID(appID)
a.SetIcon(loadAppIcon())
w := a.NewWindow("PySentry " + core.Version)
w := a.NewWindow("GoSentry " + core.Version)
configureSystemTray(a, w)
w.Resize(fyne.NewSize(1120, 720))
w.SetContent(newMainView(w, started))
@@ -81,7 +81,7 @@ func configureSystemTray(a fyne.App, w fyne.Window) {
return
}
menu := fyne.NewMenu("PySentry",
menu := fyne.NewMenu("GoSentry",
fyne.NewMenuItem("Show", func() {
w.Show()
w.RequestFocus()
@@ -146,7 +146,7 @@ func serveSingleInstance(listener net.Listener, w fyne.Window) {
func newMainView(w fyne.Window, started time.Time) fyne.CanvasObject {
store, jobs, err := core.OpenStore()
if err != nil {
return container.NewPadded(widget.NewLabel("Failed to load PySentry configuration: " + err.Error()))
return container.NewPadded(widget.NewLabel("Failed to load GoSentry configuration: " + err.Error()))
}
if iconPath, err := core.InstallDesktopIntegration(appID, store.Paths.ExecutablePath, assets.IconBytes()); err == nil {
store.Paths.DesktopIcon = iconPath
@@ -280,7 +280,7 @@ func newMainView(w fyne.Window, started time.Time) fyne.CanvasObject {
folderSelect.SetSelected(selectedFolder)
addButton := widget.NewButtonWithIcon("New job", theme.ContentAddIcon(), func() {
showJobDialog(w, "New job", job{Schedule: "@every 1m", Command: "echo PySentry job ran", Enabled: true, LastRun: "Never", NextRun: "After save", LastState: "Ready"}, func(saved job) {
showJobDialog(w, "New job", job{Schedule: "@every 1m", Command: "echo GoSentry job ran", Enabled: true, LastRun: "Never", NextRun: "After save", LastState: "Ready"}, func(saved job) {
saved.ID = nextJobID
nextJobID++
jobs = append(jobs, saved)
@@ -658,7 +658,7 @@ func showJobDialog(w fyne.Window, title string, current job, onSave func(job)) {
schedule.SetPlaceHolder("@every 1m")
schedule.SetText(current.Schedule)
command := widget.NewEntry()
command.SetPlaceHolder("echo PySentry job ran")
command.SetPlaceHolder("echo GoSentry job ran")
command.SetText(current.Command)
enabled := widget.NewCheck("Enabled", nil)
enabled.SetChecked(current.Enabled)