Rename project to GoSentry

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

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

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

Refresh README, architecture notes, roadmap, changelog, and release examples for version 0.3.0.
This commit is contained in:
mixeme
2026-06-17 07:29:58 +03:00
parent d828e34121
commit 94033e794f
28 changed files with 182 additions and 112 deletions
+43 -4
View File
@@ -11,7 +11,8 @@ import (
"strings"
)
const autostartDesktopFileName = "pysentry.desktop"
const autostartDesktopFileName = "gosentry.desktop"
const legacyAutostartDesktopFileName = "pysentry.desktop"
func SetAutostart(enabled bool, executablePath string, iconPath string) error {
desktopPath, err := autostartDesktopPath()
@@ -21,6 +22,9 @@ func SetAutostart(enabled bool, executablePath string, iconPath string) error {
if err := cleanupLegacySystemdAutostart(); err != nil {
return err
}
if err := cleanupLegacyDesktopAutostart(); err != nil {
return err
}
if enabled {
if err := os.MkdirAll(filepath.Dir(desktopPath), 0o755); err != nil {
@@ -28,8 +32,8 @@ func SetAutostart(enabled bool, executablePath string, iconPath string) error {
}
desktopFile := fmt.Sprintf(`[Desktop Entry]
Type=Application
Name=PySentry
Comment=PySentry desktop scheduler
Name=GoSentry
Comment=GoSentry desktop scheduler
Exec=%s %s
%s
Terminal=false
@@ -52,6 +56,9 @@ func AutostartStatus(expectedEnabled bool, executablePath string) (bool, string)
if legacySystemdAutostartExists() {
return false, "Legacy systemd autostart entry still exists"
}
if legacyDesktopAutostartExists() {
return false, "Legacy desktop autostart entry still exists"
}
data, readErr := os.ReadFile(desktopPath)
if !expectedEnabled {
@@ -82,6 +89,18 @@ func autostartDesktopPath() (string, error) {
return filepath.Join(configHome, "autostart", autostartDesktopFileName), nil
}
func legacyAutostartDesktopPath() (string, error) {
configHome := os.Getenv("XDG_CONFIG_HOME")
if configHome == "" {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
configHome = filepath.Join(home, ".config")
}
return filepath.Join(configHome, "autostart", legacyAutostartDesktopFileName), nil
}
func quoteDesktopExec(path string) string {
return strconv.Quote(path)
}
@@ -103,7 +122,7 @@ func cleanupLegacySystemdAutostart() error {
}
// Older PySentry builds used a systemd user unit for autostart. The current
// Linux implementation uses XDG Autostart because PySentry is a GUI/tray
// GoSentry implementation uses XDG Autostart because it is a GUI/tray
// application and should be launched by the desktop session. Disable and
// remove the old unit so the two mechanisms do not fight or start duplicates.
_ = exec.Command("systemctl", "--user", "disable", "pysentry.service").Run()
@@ -114,6 +133,26 @@ func cleanupLegacySystemdAutostart() error {
return nil
}
func cleanupLegacyDesktopAutostart() error {
desktopPath, err := legacyAutostartDesktopPath()
if err != nil {
return err
}
if err := os.Remove(desktopPath); err != nil && !os.IsNotExist(err) {
return err
}
return nil
}
func legacyDesktopAutostartExists() bool {
desktopPath, err := legacyAutostartDesktopPath()
if err != nil {
return false
}
_, err = os.Stat(desktopPath)
return err == nil
}
func legacySystemdAutostartExists() bool {
unitPath, err := legacySystemdUnitPath()
if err != nil {
+23
View File
@@ -4,6 +4,7 @@ package core
import (
"os"
"path/filepath"
"strings"
"testing"
)
@@ -30,3 +31,25 @@ func TestLinuxAutostartStartsInTray(t *testing.T) {
t.Fatalf("desktop entry does not start in tray: %s", data)
}
}
func TestLinuxAutostartRemovesLegacyDesktopEntry(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
legacyPath, err := legacyAutostartDesktopPath()
if err != nil {
t.Fatalf("resolve legacy desktop path: %v", err)
}
if err := os.MkdirAll(filepath.Dir(legacyPath), 0o755); err != nil {
t.Fatalf("create legacy desktop directory: %v", err)
}
if err := os.WriteFile(legacyPath, []byte("[Desktop Entry]\nName=PySentry\n"), 0o644); err != nil {
t.Fatalf("write legacy desktop entry: %v", err)
}
if err := SetAutostart(true, "/opt/gosentry/gosentry", ""); err != nil {
t.Fatalf("enable autostart: %v", err)
}
if _, err := os.Stat(legacyPath); !os.IsNotExist(err) {
t.Fatalf("legacy desktop entry still exists or cannot be checked: %v", err)
}
}
+3 -3
View File
@@ -11,19 +11,19 @@ import (
func TestParseRegistryRunValue(t *testing.T) {
output := `
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run
PySentry REG_SZ "D:\Apps\PySentry\pysentry.exe"
GoSentry REG_SZ "D:\Apps\GoSentry\gosentry.exe"
`
value, ok := parseRegistryRunValue(output)
if !ok {
t.Fatal("expected registry value to parse")
}
if value != `D:\Apps\PySentry\pysentry.exe` {
if value != `D:\Apps\GoSentry\gosentry.exe` {
t.Fatalf("unexpected value: %q", value)
}
}
func TestSameWindowsPathIgnoresCaseAndQuotes(t *testing.T) {
if !sameWindowsPath(`"D:\Apps\PySentry\pysentry.exe"`, `d:\apps\pysentry\pysentry.exe`) {
if !sameWindowsPath(`"D:\Apps\GoSentry\gosentry.exe"`, `d:\apps\gosentry\gosentry.exe`) {
t.Fatal("expected paths to match")
}
}
+3 -3
View File
@@ -18,7 +18,7 @@ func InstallDesktopIntegration(appID string, executablePath string, icon []byte)
// environment can match the window app id to an installed .desktop file and
// icon. Use the user's XDG data directory so portable builds do not need root
// access or a package manager install step.
iconPath := filepath.Join(dataHome, "icons", "hicolor", "256x256", "apps", "pysentry.png")
iconPath := filepath.Join(dataHome, "icons", "hicolor", "256x256", "apps", "gosentry.png")
if err := writeUserFile(iconPath, icon, 0o644); err != nil {
return "", err
}
@@ -26,8 +26,8 @@ func InstallDesktopIntegration(appID string, executablePath string, icon []byte)
desktopPath := filepath.Join(dataHome, "applications", appID+".desktop")
desktopFile := fmt.Sprintf(`[Desktop Entry]
Type=Application
Name=PySentry
Comment=PySentry desktop scheduler
Name=GoSentry
Comment=GoSentry desktop scheduler
Exec=%s
Icon=%s
Terminal=false
+2 -2
View File
@@ -7,7 +7,7 @@ import "time"
// launches omit this flag and open the normal window.
const StartInTrayArgument = "--start-in-tray"
// Config is stored in pysentry.yaml next to the program. It contains only
// Config is stored in gosentry.yaml next to the program. It contains only
// application-level choices: where to read jobs from, where to write logs, and
// how the desktop shell should behave.
type Config struct {
@@ -29,7 +29,7 @@ type JobsFile struct {
// Job is the user-visible scheduled command.
//
// Fields with yaml:"-" are deliberately runtime-only. They are useful in the GUI
// while PySentry is running, but writing them to jobs.yaml would make the jobs
// while GoSentry is running, but writing them to jobs.yaml would make the jobs
// file noisy and would mix durable configuration with transient execution state.
type Job struct {
ID int `yaml:"id"`
+5 -1
View File
@@ -8,7 +8,11 @@ import (
const (
// The config file stays beside the executable so the portable build behaves
// predictably: moving the program folder moves its settings with it.
ConfigFileName = "pysentry.yaml"
ConfigFileName = "gosentry.yaml"
// Older builds were named PySentry. Keep the old config name readable during
// the rename window so portable installations can start once and rewrite the
// settings to gosentry.yaml without manual file copying.
LegacyConfigFileName = "pysentry.yaml"
// Jobs are kept in a separate YAML file because the user can choose a
// different jobs directory, while application settings remain local to the
// installed/copied program.
+1 -1
View File
@@ -94,7 +94,7 @@ func CleanupLogs(logsDir string, maxFiles int, maxAgeDays int) error {
var logs []logFile
cutoff := time.Now().AddDate(0, 0, -maxAgeDays)
for _, entry := range entries {
// Only PySentry run logs are managed here. Directories and non-.log files
// Only GoSentry run logs are managed here. Directories and non-.log files
// are intentionally ignored so the user can keep notes or other artifacts
// in the same folder without the cleanup policy deleting them.
if entry.IsDir() || !strings.HasSuffix(strings.ToLower(entry.Name()), ".log") {
+1 -1
View File
@@ -6,7 +6,7 @@ import (
)
func configureHiddenWindow(command *exec.Cmd) {
// PySentry is a GUI scheduler, so child commands should not flash a console
// GoSentry is a GUI scheduler, so child commands should not flash a console
// window on Windows. CREATE_NO_WINDOW keeps cmd.exe and simple console tools
// quiet while stdout/stderr are still captured through pipes.
command.SysProcAttr = &syscall.SysProcAttr{
+1 -1
View File
@@ -222,7 +222,7 @@ func nextRunTime(schedule string, from time.Time) (time.Time, bool) {
}
return from.Add(interval), true
}
// Standard five-field cron keeps PySentry compatible with the mental model
// Standard five-field cron keeps GoSentry compatible with the mental model
// users already know from Unix cron, while robfig/cron handles edge cases
// such as ranges, steps, and day-of-week names.
parsed, err := cronParser.Parse(schedule)
+15 -5
View File
@@ -77,11 +77,21 @@ func loadOrCreateConfig(paths Paths) (Config, error) {
NotifyOnFailure: true,
}
if _, err := os.Stat(paths.ConfigPath); errors.Is(err, os.ErrNotExist) {
configPath := paths.ConfigPath
if _, err := os.Stat(configPath); errors.Is(err, os.ErrNotExist) {
legacyPath := filepath.Join(paths.AppDir, LegacyConfigFileName)
if _, legacyErr := os.Stat(legacyPath); legacyErr == nil {
configPath = legacyPath
} else {
return config, writeYAML(paths.ConfigPath, config)
}
}
if _, err := os.Stat(configPath); errors.Is(err, os.ErrNotExist) {
return config, writeYAML(paths.ConfigPath, config)
}
data, err := os.ReadFile(paths.ConfigPath)
data, err := os.ReadFile(configPath)
if err != nil {
return Config{}, err
}
@@ -146,7 +156,7 @@ func normalizeJobs(jobs []Job) {
if strings.TrimSpace(job.Command) == "" {
// An empty command would fail in a confusing way. A safe echo command
// gives the user something observable and harmless instead.
job.Command = echoCommand("PySentry job ran")
job.Command = echoCommand("GoSentry job ran")
}
if job.LastRun == "" {
job.LastRun = "Never"
@@ -209,7 +219,7 @@ func defaultJobs() []Job {
Name: "Hello scheduler",
Folder: "Examples",
Schedule: "@every 1m",
Command: echoCommand("PySentry test job: scheduler is alive"),
Command: echoCommand("GoSentry test job: scheduler is alive"),
Enabled: true,
},
{
@@ -217,7 +227,7 @@ func defaultJobs() []Job {
Name: "Write timestamp",
Folder: "Examples",
Schedule: "*/1 * * * *",
Command: echoCommand("PySentry test job: timestamp command ran"),
Command: echoCommand("GoSentry test job: timestamp command ran"),
Enabled: true,
},
{
+1 -1
View File
@@ -3,4 +3,4 @@ package core
// Version is the application version shown in the GUI and used by build
// scripts in artifact names. It is a var rather than a const so release builds
// can override it with Go ldflags when CI tags a build.
var Version = "0.2.5"
var Version = "0.3.0"
+8 -8
View File
@@ -12,8 +12,8 @@ import (
"strings"
"time"
"github.com/pysentry/pysentry/assets"
"github.com/pysentry/pysentry/src/core"
"gitea.mixdep.ru/mix/gosentry/assets"
"gitea.mixdep.ru/mix/gosentry/src/core"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
@@ -25,7 +25,7 @@ import (
"fyne.io/fyne/v2/widget"
)
const appID = "io.github.pysentry.desktop"
const appID = "ru.mixdep.gosentry.desktop"
const allFolders = "All"
const noFolder = "No folder"
const minJobsSidebarWidth float32 = 480
@@ -57,7 +57,7 @@ func Run(startInTray bool) {
a := app.NewWithID(appID)
a.SetIcon(loadAppIcon())
w := a.NewWindow("PySentry " + core.Version)
w := a.NewWindow("GoSentry " + core.Version)
configureSystemTray(a, w)
w.Resize(fyne.NewSize(1120, 720))
w.SetContent(newMainView(w, started))
@@ -81,7 +81,7 @@ func configureSystemTray(a fyne.App, w fyne.Window) {
return
}
menu := fyne.NewMenu("PySentry",
menu := fyne.NewMenu("GoSentry",
fyne.NewMenuItem("Show", func() {
w.Show()
w.RequestFocus()
@@ -146,7 +146,7 @@ func serveSingleInstance(listener net.Listener, w fyne.Window) {
func newMainView(w fyne.Window, started time.Time) fyne.CanvasObject {
store, jobs, err := core.OpenStore()
if err != nil {
return container.NewPadded(widget.NewLabel("Failed to load PySentry configuration: " + err.Error()))
return container.NewPadded(widget.NewLabel("Failed to load GoSentry configuration: " + err.Error()))
}
if iconPath, err := core.InstallDesktopIntegration(appID, store.Paths.ExecutablePath, assets.IconBytes()); err == nil {
store.Paths.DesktopIcon = iconPath
@@ -280,7 +280,7 @@ func newMainView(w fyne.Window, started time.Time) fyne.CanvasObject {
folderSelect.SetSelected(selectedFolder)
addButton := widget.NewButtonWithIcon("New job", theme.ContentAddIcon(), func() {
showJobDialog(w, "New job", job{Schedule: "@every 1m", Command: "echo PySentry job ran", Enabled: true, LastRun: "Never", NextRun: "After save", LastState: "Ready"}, func(saved job) {
showJobDialog(w, "New job", job{Schedule: "@every 1m", Command: "echo GoSentry job ran", Enabled: true, LastRun: "Never", NextRun: "After save", LastState: "Ready"}, func(saved job) {
saved.ID = nextJobID
nextJobID++
jobs = append(jobs, saved)
@@ -658,7 +658,7 @@ func showJobDialog(w fyne.Window, title string, current job, onSave func(job)) {
schedule.SetPlaceHolder("@every 1m")
schedule.SetText(current.Schedule)
command := widget.NewEntry()
command.SetPlaceHolder("echo PySentry job ran")
command.SetPlaceHolder("echo GoSentry job ran")
command.SetText(current.Command)
enabled := widget.NewCheck("Enabled", nil)
enabled.SetChecked(current.Enabled)