diff --git a/.dockerignore b/.dockerignore index d5d1f57..0f23be5 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,6 +2,7 @@ bin dist logs +gosentry.yaml pysentry.yaml jobs.yaml *.exe diff --git a/.gitignore b/.gitignore index 6c27080..5fea5bc 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/README.md b/README.md index b56dfce..56922ec 100644 --- a/README.md +++ b/README.md @@ -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--windows-amd64.exe. The script changes +# Builds dist\windows\gosentry--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:. Useful from hosts or CI jobs +# gitea.mixdep.ru/mix/gosentry-builder:. 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. diff --git a/assets/assets.go b/assets/assets.go index 5624ff9..0587a9e 100644 --- a/assets/assets.go +++ b/assets/assets.go @@ -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 { diff --git a/assets/pysentry-icon-16x16.png b/assets/gosentry-icon-16x16.png similarity index 100% rename from assets/pysentry-icon-16x16.png rename to assets/gosentry-icon-16x16.png diff --git a/assets/pysentry-icon-big.png b/assets/gosentry-icon-big.png similarity index 100% rename from assets/pysentry-icon-big.png rename to assets/gosentry-icon-big.png diff --git a/cmd/pysentry/main.go b/cmd/gosentry/main.go similarity index 85% rename from cmd/pysentry/main.go rename to cmd/gosentry/main.go index c318839..ab6b23d 100644 --- a/cmd/pysentry/main.go +++ b/cmd/gosentry/main.go @@ -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() { diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 6426c4a..7d757b8 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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: diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 42c92c1..d20a83a 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 3518e43..b376707 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -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: diff --git a/go.mod b/go.mod index 0b1be0e..4f55de7 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/pysentry/pysentry +module gitea.mixdep.ru/mix/gosentry go 1.22 diff --git a/packaging/windows/pysentry.rc b/packaging/windows/gosentry.rc similarity index 100% rename from packaging/windows/pysentry.rc rename to packaging/windows/gosentry.rc diff --git a/scripts/build-linux-docker.sh b/scripts/build-linux-docker.sh index 25060ff..27aa460 100644 --- a/scripts/build-linux-docker.sh +++ b/scripts/build-linux-docker.sh @@ -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. diff --git a/scripts/build-linux.sh b/scripts/build-linux.sh index 5a6394d..375d67f 100644 --- a/scripts/build-linux.sh +++ b/scripts/build-linux.sh @@ -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. diff --git a/scripts/build-release-linux.sh b/scripts/build-release-linux.sh index 080a6f9..4c1aca5 100644 --- a/scripts/build-release-linux.sh +++ b/scripts/build-release-linux.sh @@ -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]++') diff --git a/scripts/build-windows.bat b/scripts/build-windows.bat index 29c8d7b..1bc9483 100644 --- a/scripts/build-windows.bat +++ b/scripts/build-windows.bat @@ -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 diff --git a/src/core/autostart_linux.go b/src/core/autostart_linux.go index f7e18cc..856670a 100644 --- a/src/core/autostart_linux.go +++ b/src/core/autostart_linux.go @@ -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 { diff --git a/src/core/autostart_linux_test.go b/src/core/autostart_linux_test.go index facba5d..a0c3234 100644 --- a/src/core/autostart_linux_test.go +++ b/src/core/autostart_linux_test.go @@ -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) + } +} diff --git a/src/core/autostart_windows_test.go b/src/core/autostart_windows_test.go index f085eb0..782b24b 100644 --- a/src/core/autostart_windows_test.go +++ b/src/core/autostart_windows_test.go @@ -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") } } diff --git a/src/core/desktop_linux.go b/src/core/desktop_linux.go index d619c2d..c099bd4 100644 --- a/src/core/desktop_linux.go +++ b/src/core/desktop_linux.go @@ -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 diff --git a/src/core/model.go b/src/core/model.go index dd55e96..59a2eec 100644 --- a/src/core/model.go +++ b/src/core/model.go @@ -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"` diff --git a/src/core/paths.go b/src/core/paths.go index 9304fb5..ffd4735 100644 --- a/src/core/paths.go +++ b/src/core/paths.go @@ -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. diff --git a/src/core/runner.go b/src/core/runner.go index b26d329..a336f72 100644 --- a/src/core/runner.go +++ b/src/core/runner.go @@ -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") { diff --git a/src/core/runner_windows.go b/src/core/runner_windows.go index 38f0efd..52144d5 100644 --- a/src/core/runner_windows.go +++ b/src/core/runner_windows.go @@ -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{ diff --git a/src/core/scheduler.go b/src/core/scheduler.go index 9f547c3..a5cfca9 100644 --- a/src/core/scheduler.go +++ b/src/core/scheduler.go @@ -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) diff --git a/src/core/store.go b/src/core/store.go index 1b5eabf..5cfd88b 100644 --- a/src/core/store.go +++ b/src/core/store.go @@ -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, }, { diff --git a/src/core/version.go b/src/core/version.go index 94d075e..a398e89 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.5" +var Version = "0.3.0" diff --git a/src/gui/app.go b/src/gui/app.go index 2e74cfd..5acf37a 100644 --- a/src/gui/app.go +++ b/src/gui/app.go @@ -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)