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:
@@ -2,6 +2,7 @@
|
|||||||
bin
|
bin
|
||||||
dist
|
dist
|
||||||
logs
|
logs
|
||||||
|
gosentry.yaml
|
||||||
pysentry.yaml
|
pysentry.yaml
|
||||||
jobs.yaml
|
jobs.yaml
|
||||||
*.exe
|
*.exe
|
||||||
|
|||||||
+3
-2
@@ -1,14 +1,15 @@
|
|||||||
# Build outputs
|
# Build outputs
|
||||||
dist/
|
dist/
|
||||||
|
|
||||||
# Generated Windows resource compiled from packaging/windows/pysentry.rc.
|
# Generated Windows resource compiled from packaging/windows/gosentry.rc.
|
||||||
cmd/pysentry/*.syso
|
cmd/gosentry/*.syso
|
||||||
|
|
||||||
# Local binaries that may be produced by ad-hoc go build commands.
|
# Local binaries that may be produced by ad-hoc go build commands.
|
||||||
*.exe
|
*.exe
|
||||||
*.test
|
*.test
|
||||||
|
|
||||||
# Runtime files created next to the executable during local runs.
|
# Runtime files created next to the executable during local runs.
|
||||||
|
gosentry.yaml
|
||||||
pysentry.yaml
|
pysentry.yaml
|
||||||
jobs.yaml
|
jobs.yaml
|
||||||
logs/
|
logs/
|
||||||
|
|||||||
@@ -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:
|
Project notes:
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ sudo apt install golang gcc libgl1-mesa-dev xorg-dev
|
|||||||
Windows:
|
Windows:
|
||||||
|
|
||||||
```powershell
|
```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
|
# 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
|
# 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
|
# when windres is available, and uses the Windows GUI subsystem so no console
|
||||||
@@ -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.5-windows-amd64.exe
|
dist\windows\gosentry-0.3.0-windows-amd64.exe
|
||||||
```
|
```
|
||||||
|
|
||||||
Linux:
|
Linux:
|
||||||
@@ -101,14 +101,14 @@ 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.5-linux-amd64
|
dist/linux/gosentry-0.3.0-linux-amd64
|
||||||
```
|
```
|
||||||
|
|
||||||
Linux using Docker:
|
Linux using Docker:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Builds the Linux binary inside Docker using the versioned image tag
|
# 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.
|
# where the native Linux/Fyne packages are not installed locally.
|
||||||
chmod +x ./scripts/build-linux-docker.sh
|
chmod +x ./scripts/build-linux-docker.sh
|
||||||
./scripts/build-linux-docker.sh
|
./scripts/build-linux-docker.sh
|
||||||
@@ -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.5-linux-amd64
|
dist\linux\gosentry-0.3.0-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.5-linux-amd64
|
dist/linux/gosentry-0.3.0-linux-amd64
|
||||||
|
|
||||||
# Linux arm64 artifact.
|
# 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.
|
# 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
|
## 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
|
# go run starts the app from source. Use scripts\build-windows.bat when you need
|
||||||
# a standalone .exe without a console window.
|
# 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:
|
Linux:
|
||||||
@@ -172,14 +172,14 @@ Linux:
|
|||||||
```bash
|
```bash
|
||||||
# CGO must stay enabled because the Fyne GUI links against native Linux desktop
|
# CGO must stay enabled because the Fyne GUI links against native Linux desktop
|
||||||
# libraries.
|
# libraries.
|
||||||
CGO_ENABLED=1 go run ./cmd/pysentry
|
CGO_ENABLED=1 go run ./cmd/gosentry
|
||||||
```
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Windows, VirtualBox, RDP, And OpenGL
|
### 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
|
desktop window. In a Windows virtual machine, especially when the session is
|
||||||
opened through RDP inside VirtualBox, the available video driver can fail OpenGL
|
opened through RDP inside VirtualBox, the available video driver can fail OpenGL
|
||||||
initialization.
|
initialization.
|
||||||
@@ -202,11 +202,11 @@ Known workaround:
|
|||||||
checksum files are not needed for this workaround.
|
checksum files are not needed for this workaround.
|
||||||
2. Open the downloaded archive and use the `x64` build from it.
|
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
|
3. Copy the Mesa OpenGL DLL files from `x64` into the same directory as the
|
||||||
PySentry `.exe`, for example:
|
GoSentry `.exe`, for example:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
dist\windows\
|
dist\windows\
|
||||||
pysentry-0.2.5-windows-amd64.exe
|
gosentry-0.3.0-windows-amd64.exe
|
||||||
opengl32.dll
|
opengl32.dll
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
@@ -217,12 +217,12 @@ driver does not provide usable OpenGL.
|
|||||||
|
|
||||||
## Storage
|
## 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
|
```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.
|
# executable lives"; an absolute path can be used when jobs should live elsewhere.
|
||||||
jobs_dir: .
|
jobs_dir: .
|
||||||
|
|
||||||
@@ -236,7 +236,7 @@ max_log_files: 100
|
|||||||
# Delete .log files older than this many days during cleanup.
|
# Delete .log files older than this many days during cleanup.
|
||||||
max_log_age_days: 30
|
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
|
start_on_login: false
|
||||||
|
|
||||||
# Closing the window hides it to the tray instead of stopping the scheduler.
|
# Closing the window hides it to the tray instead of stopping the scheduler.
|
||||||
@@ -267,7 +267,7 @@ jobs:
|
|||||||
schedule: '@every 1m'
|
schedule: '@every 1m'
|
||||||
|
|
||||||
# Command passed to the platform shell: cmd.exe /C on Windows, sh -c on Linux.
|
# 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.
|
# Disabled jobs remain in jobs.yaml but are skipped by the scheduler.
|
||||||
enabled: true
|
enabled: true
|
||||||
@@ -302,7 +302,7 @@ Standard 5-field cron schedules:
|
|||||||
|
|
||||||
## Using The App
|
## Using The App
|
||||||
|
|
||||||
1. Start PySentry.
|
1. Start GoSentry.
|
||||||
2. Use `New job` to create a command.
|
2. Use `New job` to create a command.
|
||||||
3. Set `Schedule`, `Command`, optional `Folder`, and `Enabled`.
|
3. Set `Schedule`, `Command`, optional `Folder`, and `Enabled`.
|
||||||
4. Use `Run now` for a manual test run.
|
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
|
## 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:
|
Linux:
|
||||||
|
|
||||||
```ini
|
```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
|
# This is better for a GUI/tray application than a systemd user service because
|
||||||
# the desktop environment starts it inside the graphical user session.
|
# the desktop environment starts it inside the graphical user session.
|
||||||
# Saving the setting also removes the old ~/.config/systemd/user/pysentry.service
|
# Saving the setting also removes the old ~/.config/systemd/user/pysentry.service
|
||||||
# unit if it was created by an earlier PySentry build.
|
# unit if it was created by an earlier GoSentry build.
|
||||||
~/.config/autostart/pysentry.desktop
|
~/.config/autostart/gosentry.desktop
|
||||||
|
|
||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Type=Application
|
Type=Application
|
||||||
Name=PySentry
|
Name=GoSentry
|
||||||
Exec=/opt/pysentry/pysentry-0.2.5-linux-amd64 --start-in-tray
|
Exec=/opt/gosentry/gosentry-0.3.0-linux-amd64 --start-in-tray
|
||||||
Terminal=false
|
Terminal=false
|
||||||
```
|
```
|
||||||
|
|
||||||
Windows:
|
Windows:
|
||||||
|
|
||||||
```text
|
```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,
|
# 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
|
# 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
|
# fragile command-line quoting. Saving settings rewrites the shortcut and removes
|
||||||
@@ -350,7 +350,7 @@ Windows:
|
|||||||
|
|
||||||
## Project Layout
|
## Project Layout
|
||||||
|
|
||||||
- `cmd/pysentry` starts the desktop app.
|
- `cmd/gosentry` starts the desktop app.
|
||||||
- `src/gui` contains the GUI.
|
- `src/gui` contains the GUI.
|
||||||
- `src/core` contains YAML storage, command execution, scheduling, and log cleanup.
|
- `src/core` contains YAML storage, command execution, scheduling, and log cleanup.
|
||||||
- `assets` contains app icons that are embedded into the application binary.
|
- `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
|
## 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.
|
- [`fyne.io/fyne/v2`](https://fyne.io/) for the native GUI.
|
||||||
- `github.com/robfig/cron/v3` for cron schedule parsing.
|
- `github.com/robfig/cron/v3` for cron schedule parsing.
|
||||||
|
|||||||
+2
-2
@@ -14,7 +14,7 @@ import (
|
|||||||
// The blank import enables the compiler directive below; no runtime package
|
// The blank import enables the compiler directive below; no runtime package
|
||||||
// initialization from embed is required.
|
// initialization from embed is required.
|
||||||
//
|
//
|
||||||
//go:embed pysentry-icon-big.png
|
//go:embed gosentry-icon-big.png
|
||||||
var iconBytes []byte
|
var iconBytes []byte
|
||||||
|
|
||||||
func Icon() fyne.Resource {
|
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
|
// 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
|
// by the build script through the .ico resource, because Explorer reads PE
|
||||||
// resources rather than Fyne runtime state.
|
// 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 {
|
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 (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/pysentry/pysentry/src/core"
|
"gitea.mixdep.ru/mix/gosentry/src/core"
|
||||||
"github.com/pysentry/pysentry/src/gui"
|
"gitea.mixdep.ru/mix/gosentry/src/gui"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -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
|
single desktop process: the GUI, scheduler, storage, and command runner live in
|
||||||
one application and communicate through Go function calls and shared in-memory
|
one application and communicate through Go function calls and shared in-memory
|
||||||
job state.
|
job state.
|
||||||
@@ -15,7 +15,7 @@ flowchart LR
|
|||||||
scheduler["src/core Scheduler - @every and cron timing"]
|
scheduler["src/core Scheduler - @every and cron timing"]
|
||||||
runner["src/core Runner - shell command execution"]
|
runner["src/core Runner - shell command execution"]
|
||||||
autostart["src/core Autostart - Windows Startup shortcut / Linux desktop startup"]
|
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"]
|
jobs["jobs.yaml - job definitions"]
|
||||||
logs["logs_dir - per-run command output logs"]
|
logs["logs_dir - per-run command output logs"]
|
||||||
shell["Platform shell - cmd.exe /C or sh -c"]
|
shell["Platform shell - cmd.exe /C or sh -c"]
|
||||||
@@ -41,8 +41,8 @@ flowchart LR
|
|||||||
## Main Flows
|
## Main Flows
|
||||||
|
|
||||||
1. Startup:
|
1. Startup:
|
||||||
The executable starts `cmd/pysentry`, which calls the GUI package. The GUI
|
The executable starts `cmd/gosentry`, which calls the GUI package. The GUI
|
||||||
opens the store, loads `pysentry.yaml` and `jobs.yaml`, creates the main tabs,
|
opens the store, loads `gosentry.yaml` and `jobs.yaml`, creates the main tabs,
|
||||||
then starts the scheduler with the loaded job slice.
|
then starts the scheduler with the loaded job slice.
|
||||||
|
|
||||||
2. Editing settings or jobs:
|
2. Editing settings or jobs:
|
||||||
|
|||||||
+10
-2
@@ -1,6 +1,14 @@
|
|||||||
# Changelog
|
# 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
|
## 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
|
## 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 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/`.
|
- 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.
|
- Adjusted the Mermaid architecture diagram to avoid line-break syntax that breaks rendering in Gitea.
|
||||||
|
|||||||
+4
-20
@@ -1,22 +1,6 @@
|
|||||||
# Roadmap
|
# Roadmap
|
||||||
|
|
||||||
This file tracks planned PySentry work that is larger than a single bug fix.
|
This file tracks planned GoSentry 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.
|
|
||||||
|
|
||||||
## Post-Field-Test Cleanup
|
## Post-Field-Test Cleanup
|
||||||
|
|
||||||
@@ -56,7 +40,7 @@ runtime YAML files live next to the executable by default.
|
|||||||
|
|
||||||
Planned delivery variants:
|
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`.
|
- Linux portable `.tar.gz` archives for `linux-amd64` and `linux-arm64`.
|
||||||
- Debian/Ubuntu `.deb` package once the Linux runtime paths are settled.
|
- Debian/Ubuntu `.deb` package once the Linux runtime paths are settled.
|
||||||
- Windows installer later, likely Inno Setup first and MSI/WiX only if needed.
|
- 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.
|
- Portable builds can keep settings and jobs next to the executable.
|
||||||
- Installer/package builds should move runtime data to per-user locations:
|
- Installer/package builds should move runtime data to per-user locations:
|
||||||
`%APPDATA%\PySentry` on Windows, and XDG directories such as
|
`%APPDATA%\GoSentry` on Windows, and XDG directories such as
|
||||||
`~/.config/pysentry` and `~/.local/share/pysentry` on Linux.
|
`~/.config/gosentry` and `~/.local/share/gosentry` on Linux.
|
||||||
|
|
||||||
Initial priority:
|
Initial priority:
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
module github.com/pysentry/pysentry
|
module gitea.mixdep.ru/mix/gosentry
|
||||||
|
|
||||||
go 1.22
|
go 1.22
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ set -euo pipefail
|
|||||||
# default includes the application version and target platform.
|
# default includes the application version and target platform.
|
||||||
version="$(sed -n 's/^var Version = "\(.*\)"/\1/p' src/core/version.go)"
|
version="$(sed -n 's/^var Version = "\(.*\)"/\1/p' src/core/version.go)"
|
||||||
version="${version:-0.0.0-dev}"
|
version="${version:-0.0.0-dev}"
|
||||||
tag="gitea.mixdep.ru/mix/pysentry-builder:${version}"
|
tag="gitea.mixdep.ru/mix/gosentry-builder:${version}"
|
||||||
output="${1:-dist/linux/pysentry-${version}-linux-amd64}"
|
output="${1:-dist/linux/gosentry-${version}-linux-amd64}"
|
||||||
docker_user_args=()
|
docker_user_args=()
|
||||||
if command -v id >/dev/null 2>&1; then
|
if command -v id >/dev/null 2>&1; then
|
||||||
docker_user_args=(--user "$(id -u):$(id -g)")
|
docker_user_args=(--user "$(id -u):$(id -g)")
|
||||||
@@ -26,7 +26,7 @@ docker run --rm \
|
|||||||
-v "$(pwd):/src" \
|
-v "$(pwd):/src" \
|
||||||
-w /src \
|
-w /src \
|
||||||
"$tag" \
|
"$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
|
# Icons are embedded in the Go binary, so there is no assets directory to copy
|
||||||
# after extracting the Linux executable.
|
# after extracting the Linux executable.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ set -euo pipefail
|
|||||||
# default includes the application version and target platform.
|
# default includes the application version and target platform.
|
||||||
version="$(sed -n 's/^var Version = "\(.*\)"/\1/p' src/core/version.go)"
|
version="$(sed -n 's/^var Version = "\(.*\)"/\1/p' src/core/version.go)"
|
||||||
version="${version:-0.0.0-dev}"
|
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")"
|
mkdir -p "$(dirname "$output")"
|
||||||
|
|
||||||
# Fyne needs CGO for its native desktop backend. The script pins the target to
|
# 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
|
# -trimpath removes local machine paths from debug/build metadata. -s -w strips
|
||||||
# symbol/debug tables to keep the desktop binary smaller.
|
# 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
|
# The application icon is embedded by Go, so the Linux build does not need a
|
||||||
# sidecar assets directory beside the executable.
|
# sidecar assets directory beside the executable.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ cd "$repo_root"
|
|||||||
|
|
||||||
version="$(sed -n 's/^var Version = "\(.*\)"/\1/p' src/core/version.go)"
|
version="$(sed -n 's/^var Version = "\(.*\)"/\1/p' src/core/version.go)"
|
||||||
version="${version:-0.0.0-dev}"
|
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=()
|
docker_user_args=()
|
||||||
if command -v id >/dev/null 2>&1; then
|
if command -v id >/dev/null 2>&1; then
|
||||||
@@ -24,9 +24,9 @@ Usage: $0 [target...]
|
|||||||
|
|
||||||
Targets:
|
Targets:
|
||||||
all Build every release artifact.
|
all Build every release artifact.
|
||||||
linux-amd64 Build dist/linux/pysentry-${version}-linux-amd64.
|
linux-amd64 Build dist/linux/gosentry-${version}-linux-amd64.
|
||||||
linux-arm64 Build dist/linux/pysentry-${version}-linux-arm64.
|
linux-arm64 Build dist/linux/gosentry-${version}-linux-arm64.
|
||||||
windows-amd64 Build dist/windows/pysentry-${version}-windows-amd64.exe.
|
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.
|
When no target is passed and the script runs in a terminal, it asks what to build.
|
||||||
EOF
|
EOF
|
||||||
@@ -99,15 +99,15 @@ run_in_builder() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
build_linux_amd64() {
|
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() {
|
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() {
|
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]++')
|
mapfile -t targets < <(choose_targets "$@" | normalize_targets | awk '!seen[$0]++')
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ setlocal enabledelayedexpansion
|
|||||||
|
|
||||||
REM Double-clicking a .bat file can start it with an arbitrary working
|
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 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\.."
|
cd /d "%~dp0\.."
|
||||||
|
|
||||||
for /f "tokens=4" %%V in ('findstr /C:"var Version" src\core\version.go') do set "VERSION=%%~V"
|
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 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.
|
REM stays clean and the old bin\ folder is no longer needed.
|
||||||
set "OUTPUT=%~1"
|
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 Prefer the standard Go installer path on Windows, but fall back to PATH for
|
||||||
REM machines where Go was installed by another package manager.
|
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%"
|
if not exist "%OUTDIR%" mkdir "%OUTDIR%"
|
||||||
|
|
||||||
REM windres embeds the .ico file into the PE executable so Windows Explorer,
|
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.
|
REM handles Fyne's runtime icon, but Explorer reads this Windows resource instead.
|
||||||
where windres.exe >nul 2>nul
|
where windres.exe >nul 2>nul
|
||||||
if %ERRORLEVEL%==0 (
|
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 -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 size, and -H=windowsgui prevents a separate console window from opening when
|
||||||
REM the GUI app starts from Explorer or a shortcut.
|
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
|
if errorlevel 1 exit /b 1
|
||||||
|
|
||||||
REM Icons are embedded into the executable, so no assets directory is copied next
|
REM Icons are embedded into the executable, so no assets directory is copied next
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const autostartDesktopFileName = "pysentry.desktop"
|
const autostartDesktopFileName = "gosentry.desktop"
|
||||||
|
const legacyAutostartDesktopFileName = "pysentry.desktop"
|
||||||
|
|
||||||
func SetAutostart(enabled bool, executablePath string, iconPath string) error {
|
func SetAutostart(enabled bool, executablePath string, iconPath string) error {
|
||||||
desktopPath, err := autostartDesktopPath()
|
desktopPath, err := autostartDesktopPath()
|
||||||
@@ -21,6 +22,9 @@ func SetAutostart(enabled bool, executablePath string, iconPath string) error {
|
|||||||
if err := cleanupLegacySystemdAutostart(); err != nil {
|
if err := cleanupLegacySystemdAutostart(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := cleanupLegacyDesktopAutostart(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if enabled {
|
if enabled {
|
||||||
if err := os.MkdirAll(filepath.Dir(desktopPath), 0o755); err != nil {
|
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]
|
desktopFile := fmt.Sprintf(`[Desktop Entry]
|
||||||
Type=Application
|
Type=Application
|
||||||
Name=PySentry
|
Name=GoSentry
|
||||||
Comment=PySentry desktop scheduler
|
Comment=GoSentry desktop scheduler
|
||||||
Exec=%s %s
|
Exec=%s %s
|
||||||
%s
|
%s
|
||||||
Terminal=false
|
Terminal=false
|
||||||
@@ -52,6 +56,9 @@ func AutostartStatus(expectedEnabled bool, executablePath string) (bool, string)
|
|||||||
if legacySystemdAutostartExists() {
|
if legacySystemdAutostartExists() {
|
||||||
return false, "Legacy systemd autostart entry still exists"
|
return false, "Legacy systemd autostart entry still exists"
|
||||||
}
|
}
|
||||||
|
if legacyDesktopAutostartExists() {
|
||||||
|
return false, "Legacy desktop autostart entry still exists"
|
||||||
|
}
|
||||||
data, readErr := os.ReadFile(desktopPath)
|
data, readErr := os.ReadFile(desktopPath)
|
||||||
|
|
||||||
if !expectedEnabled {
|
if !expectedEnabled {
|
||||||
@@ -82,6 +89,18 @@ func autostartDesktopPath() (string, error) {
|
|||||||
return filepath.Join(configHome, "autostart", autostartDesktopFileName), nil
|
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 {
|
func quoteDesktopExec(path string) string {
|
||||||
return strconv.Quote(path)
|
return strconv.Quote(path)
|
||||||
}
|
}
|
||||||
@@ -103,7 +122,7 @@ func cleanupLegacySystemdAutostart() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Older PySentry builds used a systemd user unit for autostart. The current
|
// 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
|
// 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.
|
// remove the old unit so the two mechanisms do not fight or start duplicates.
|
||||||
_ = exec.Command("systemctl", "--user", "disable", "pysentry.service").Run()
|
_ = exec.Command("systemctl", "--user", "disable", "pysentry.service").Run()
|
||||||
@@ -114,6 +133,26 @@ func cleanupLegacySystemdAutostart() error {
|
|||||||
return nil
|
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 {
|
func legacySystemdAutostartExists() bool {
|
||||||
unitPath, err := legacySystemdUnitPath()
|
unitPath, err := legacySystemdUnitPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package core
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -30,3 +31,25 @@ func TestLinuxAutostartStartsInTray(t *testing.T) {
|
|||||||
t.Fatalf("desktop entry does not start in tray: %s", data)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,19 +11,19 @@ import (
|
|||||||
func TestParseRegistryRunValue(t *testing.T) {
|
func TestParseRegistryRunValue(t *testing.T) {
|
||||||
output := `
|
output := `
|
||||||
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run
|
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)
|
value, ok := parseRegistryRunValue(output)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("expected registry value to parse")
|
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)
|
t.Fatalf("unexpected value: %q", value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSameWindowsPathIgnoresCaseAndQuotes(t *testing.T) {
|
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")
|
t.Fatal("expected paths to match")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
// 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
|
// icon. Use the user's XDG data directory so portable builds do not need root
|
||||||
// access or a package manager install step.
|
// 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 {
|
if err := writeUserFile(iconPath, icon, 0o644); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -26,8 +26,8 @@ func InstallDesktopIntegration(appID string, executablePath string, icon []byte)
|
|||||||
desktopPath := filepath.Join(dataHome, "applications", appID+".desktop")
|
desktopPath := filepath.Join(dataHome, "applications", appID+".desktop")
|
||||||
desktopFile := fmt.Sprintf(`[Desktop Entry]
|
desktopFile := fmt.Sprintf(`[Desktop Entry]
|
||||||
Type=Application
|
Type=Application
|
||||||
Name=PySentry
|
Name=GoSentry
|
||||||
Comment=PySentry desktop scheduler
|
Comment=GoSentry desktop scheduler
|
||||||
Exec=%s
|
Exec=%s
|
||||||
Icon=%s
|
Icon=%s
|
||||||
Terminal=false
|
Terminal=false
|
||||||
|
|||||||
+2
-2
@@ -7,7 +7,7 @@ import "time"
|
|||||||
// launches omit this flag and open the normal window.
|
// launches omit this flag and open the normal window.
|
||||||
const StartInTrayArgument = "--start-in-tray"
|
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
|
// application-level choices: where to read jobs from, where to write logs, and
|
||||||
// how the desktop shell should behave.
|
// how the desktop shell should behave.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -29,7 +29,7 @@ type JobsFile struct {
|
|||||||
// Job is the user-visible scheduled command.
|
// Job is the user-visible scheduled command.
|
||||||
//
|
//
|
||||||
// Fields with yaml:"-" are deliberately runtime-only. They are useful in the GUI
|
// 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.
|
// file noisy and would mix durable configuration with transient execution state.
|
||||||
type Job struct {
|
type Job struct {
|
||||||
ID int `yaml:"id"`
|
ID int `yaml:"id"`
|
||||||
|
|||||||
+5
-1
@@ -8,7 +8,11 @@ import (
|
|||||||
const (
|
const (
|
||||||
// The config file stays beside the executable so the portable build behaves
|
// The config file stays beside the executable so the portable build behaves
|
||||||
// predictably: moving the program folder moves its settings with it.
|
// 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
|
// Jobs are kept in a separate YAML file because the user can choose a
|
||||||
// different jobs directory, while application settings remain local to the
|
// different jobs directory, while application settings remain local to the
|
||||||
// installed/copied program.
|
// installed/copied program.
|
||||||
|
|||||||
+1
-1
@@ -94,7 +94,7 @@ func CleanupLogs(logsDir string, maxFiles int, maxAgeDays int) error {
|
|||||||
var logs []logFile
|
var logs []logFile
|
||||||
cutoff := time.Now().AddDate(0, 0, -maxAgeDays)
|
cutoff := time.Now().AddDate(0, 0, -maxAgeDays)
|
||||||
for _, entry := range entries {
|
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
|
// are intentionally ignored so the user can keep notes or other artifacts
|
||||||
// in the same folder without the cleanup policy deleting them.
|
// in the same folder without the cleanup policy deleting them.
|
||||||
if entry.IsDir() || !strings.HasSuffix(strings.ToLower(entry.Name()), ".log") {
|
if entry.IsDir() || !strings.HasSuffix(strings.ToLower(entry.Name()), ".log") {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func configureHiddenWindow(command *exec.Cmd) {
|
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
|
// window on Windows. CREATE_NO_WINDOW keeps cmd.exe and simple console tools
|
||||||
// quiet while stdout/stderr are still captured through pipes.
|
// quiet while stdout/stderr are still captured through pipes.
|
||||||
command.SysProcAttr = &syscall.SysProcAttr{
|
command.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ func nextRunTime(schedule string, from time.Time) (time.Time, bool) {
|
|||||||
}
|
}
|
||||||
return from.Add(interval), true
|
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
|
// users already know from Unix cron, while robfig/cron handles edge cases
|
||||||
// such as ranges, steps, and day-of-week names.
|
// such as ranges, steps, and day-of-week names.
|
||||||
parsed, err := cronParser.Parse(schedule)
|
parsed, err := cronParser.Parse(schedule)
|
||||||
|
|||||||
+15
-5
@@ -77,11 +77,21 @@ func loadOrCreateConfig(paths Paths) (Config, error) {
|
|||||||
NotifyOnFailure: true,
|
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)
|
return config, writeYAML(paths.ConfigPath, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := os.ReadFile(paths.ConfigPath)
|
data, err := os.ReadFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Config{}, err
|
return Config{}, err
|
||||||
}
|
}
|
||||||
@@ -146,7 +156,7 @@ func normalizeJobs(jobs []Job) {
|
|||||||
if strings.TrimSpace(job.Command) == "" {
|
if strings.TrimSpace(job.Command) == "" {
|
||||||
// An empty command would fail in a confusing way. A safe echo command
|
// An empty command would fail in a confusing way. A safe echo command
|
||||||
// gives the user something observable and harmless instead.
|
// gives the user something observable and harmless instead.
|
||||||
job.Command = echoCommand("PySentry job ran")
|
job.Command = echoCommand("GoSentry job ran")
|
||||||
}
|
}
|
||||||
if job.LastRun == "" {
|
if job.LastRun == "" {
|
||||||
job.LastRun = "Never"
|
job.LastRun = "Never"
|
||||||
@@ -209,7 +219,7 @@ func defaultJobs() []Job {
|
|||||||
Name: "Hello scheduler",
|
Name: "Hello scheduler",
|
||||||
Folder: "Examples",
|
Folder: "Examples",
|
||||||
Schedule: "@every 1m",
|
Schedule: "@every 1m",
|
||||||
Command: echoCommand("PySentry test job: scheduler is alive"),
|
Command: echoCommand("GoSentry test job: scheduler is alive"),
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -217,7 +227,7 @@ func defaultJobs() []Job {
|
|||||||
Name: "Write timestamp",
|
Name: "Write timestamp",
|
||||||
Folder: "Examples",
|
Folder: "Examples",
|
||||||
Schedule: "*/1 * * * *",
|
Schedule: "*/1 * * * *",
|
||||||
Command: echoCommand("PySentry test job: timestamp command ran"),
|
Command: echoCommand("GoSentry test job: timestamp command ran"),
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+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.5"
|
var Version = "0.3.0"
|
||||||
|
|||||||
+8
-8
@@ -12,8 +12,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pysentry/pysentry/assets"
|
"gitea.mixdep.ru/mix/gosentry/assets"
|
||||||
"github.com/pysentry/pysentry/src/core"
|
"gitea.mixdep.ru/mix/gosentry/src/core"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/app"
|
"fyne.io/fyne/v2/app"
|
||||||
@@ -25,7 +25,7 @@ import (
|
|||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
)
|
)
|
||||||
|
|
||||||
const appID = "io.github.pysentry.desktop"
|
const appID = "ru.mixdep.gosentry.desktop"
|
||||||
const allFolders = "All"
|
const allFolders = "All"
|
||||||
const noFolder = "No folder"
|
const noFolder = "No folder"
|
||||||
const minJobsSidebarWidth float32 = 480
|
const minJobsSidebarWidth float32 = 480
|
||||||
@@ -57,7 +57,7 @@ func Run(startInTray bool) {
|
|||||||
a := app.NewWithID(appID)
|
a := app.NewWithID(appID)
|
||||||
a.SetIcon(loadAppIcon())
|
a.SetIcon(loadAppIcon())
|
||||||
|
|
||||||
w := a.NewWindow("PySentry " + core.Version)
|
w := a.NewWindow("GoSentry " + core.Version)
|
||||||
configureSystemTray(a, w)
|
configureSystemTray(a, w)
|
||||||
w.Resize(fyne.NewSize(1120, 720))
|
w.Resize(fyne.NewSize(1120, 720))
|
||||||
w.SetContent(newMainView(w, started))
|
w.SetContent(newMainView(w, started))
|
||||||
@@ -81,7 +81,7 @@ func configureSystemTray(a fyne.App, w fyne.Window) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
menu := fyne.NewMenu("PySentry",
|
menu := fyne.NewMenu("GoSentry",
|
||||||
fyne.NewMenuItem("Show", func() {
|
fyne.NewMenuItem("Show", func() {
|
||||||
w.Show()
|
w.Show()
|
||||||
w.RequestFocus()
|
w.RequestFocus()
|
||||||
@@ -146,7 +146,7 @@ func serveSingleInstance(listener net.Listener, w fyne.Window) {
|
|||||||
func newMainView(w fyne.Window, started time.Time) fyne.CanvasObject {
|
func newMainView(w fyne.Window, started time.Time) fyne.CanvasObject {
|
||||||
store, jobs, err := core.OpenStore()
|
store, jobs, err := core.OpenStore()
|
||||||
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 GoSentry configuration: " + err.Error()))
|
||||||
}
|
}
|
||||||
if iconPath, err := core.InstallDesktopIntegration(appID, store.Paths.ExecutablePath, assets.IconBytes()); err == nil {
|
if iconPath, err := core.InstallDesktopIntegration(appID, store.Paths.ExecutablePath, assets.IconBytes()); err == nil {
|
||||||
store.Paths.DesktopIcon = iconPath
|
store.Paths.DesktopIcon = iconPath
|
||||||
@@ -280,7 +280,7 @@ func newMainView(w fyne.Window, started time.Time) fyne.CanvasObject {
|
|||||||
folderSelect.SetSelected(selectedFolder)
|
folderSelect.SetSelected(selectedFolder)
|
||||||
|
|
||||||
addButton := widget.NewButtonWithIcon("New job", theme.ContentAddIcon(), func() {
|
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
|
saved.ID = nextJobID
|
||||||
nextJobID++
|
nextJobID++
|
||||||
jobs = append(jobs, saved)
|
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.SetPlaceHolder("@every 1m")
|
||||||
schedule.SetText(current.Schedule)
|
schedule.SetText(current.Schedule)
|
||||||
command := widget.NewEntry()
|
command := widget.NewEntry()
|
||||||
command.SetPlaceHolder("echo PySentry job ran")
|
command.SetPlaceHolder("echo GoSentry job ran")
|
||||||
command.SetText(current.Command)
|
command.SetText(current.Command)
|
||||||
enabled := widget.NewCheck("Enabled", nil)
|
enabled := widget.NewCheck("Enabled", nil)
|
||||||
enabled.SetChecked(current.Enabled)
|
enabled.SetChecked(current.Enabled)
|
||||||
|
|||||||
Reference in New Issue
Block a user