Compare commits
3 Commits
079961e735
...
b1fe8bd675
| Author | SHA1 | Date | |
|---|---|---|---|
| b1fe8bd675 | |||
| 44f24ab3d8 | |||
| 0bc9e91d1e |
@@ -274,6 +274,7 @@ Standard 5-field cron schedules:
|
|||||||
Changing `jobs_dir` saves the current job list to the new directory.
|
Changing `jobs_dir` saves the current job list to the new directory.
|
||||||
|
|
||||||
The `Start on login` setting shows an `OK` or `Problem` status next to the checkbox. Saving settings with the checkbox enabled rewrites the autostart entry using the current executable path.
|
The `Start on login` setting shows an `OK` or `Problem` status next to the checkbox. Saving settings with the checkbox enabled rewrites the autostart entry using the current executable path.
|
||||||
|
Autostart entries add `--start-in-tray`, so scheduled jobs begin running after sign-in without opening the main window.
|
||||||
|
|
||||||
## Autostart
|
## Autostart
|
||||||
|
|
||||||
@@ -292,19 +293,19 @@ Linux:
|
|||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Type=Application
|
Type=Application
|
||||||
Name=PySentry
|
Name=PySentry
|
||||||
Exec=/opt/pysentry/pysentry-0.2.4-linux-amd64
|
Exec=/opt/pysentry/pysentry-0.2.4-linux-amd64 --start-in-tray
|
||||||
Terminal=false
|
Terminal=false
|
||||||
```
|
```
|
||||||
|
|
||||||
Windows:
|
Windows:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
# PySentry writes an HKCU Run entry when Start on login is enabled. It needs no
|
# PySentry writes a shortcut to the current user's Startup folder when Start on
|
||||||
# administrator rights and starts PySentry when the current user signs in. Task
|
# login is enabled. A .lnk stores the executable path as a structured TargetPath,
|
||||||
# Scheduler remains a later option if delayed start or elevated tasks become
|
# and stores --start-in-tray as Arguments, so paths with spaces do not need
|
||||||
# necessary. Saving settings with the checkbox enabled rewrites this entry, so it
|
# fragile command-line quoting. Saving settings rewrites the shortcut and removes
|
||||||
# repairs an old path after the executable was moved or renamed.
|
# old HKCU Run entries from earlier builds.
|
||||||
HKCU\Software\Microsoft\Windows\CurrentVersion\Run\PySentry
|
%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\GoSentry.lnk
|
||||||
```
|
```
|
||||||
|
|
||||||
## Project Layout
|
## Project Layout
|
||||||
|
|||||||
+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.png
|
//go:embed pysentry-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.png", iconBytes)
|
return fyne.NewStaticResource("pysentry-icon-big.png", iconBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func IconBytes() []byte {
|
func IconBytes() []byte {
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
+16
-2
@@ -1,10 +1,24 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "github.com/pysentry/pysentry/src/gui"
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/pysentry/pysentry/src/core"
|
||||||
|
"github.com/pysentry/pysentry/src/gui"
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// The executable entry point intentionally delegates all startup work to the
|
// The executable entry point intentionally delegates all startup work to the
|
||||||
// GUI package. Keeping main small makes it easier to add platform-specific
|
// GUI package. Keeping main small makes it easier to add platform-specific
|
||||||
// packaging later without mixing window setup, storage, and scheduler logic.
|
// packaging later without mixing window setup, storage, and scheduler logic.
|
||||||
gui.Run()
|
gui.Run(hasArgument(core.StartInTrayArgument))
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasArgument(argument string) bool {
|
||||||
|
for _, current := range os.Args[1:] {
|
||||||
|
if current == argument {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ flowchart LR
|
|||||||
store["src/core Store - YAML config and jobs"]
|
store["src/core Store - YAML config and jobs"]
|
||||||
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 Run / Linux desktop startup"]
|
autostart["src/core Autostart - Windows Startup shortcut / Linux desktop startup"]
|
||||||
config["pysentry.yaml - application settings"]
|
config["pysentry.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"]
|
||||||
@@ -67,5 +67,7 @@ flowchart LR
|
|||||||
runs log cleanup, and calls the GUI callback so the `History` tab refreshes.
|
runs log cleanup, and calls the GUI callback so the `History` tab refreshes.
|
||||||
|
|
||||||
7. Autostart:
|
7. Autostart:
|
||||||
The Settings tab calls the platform autostart implementation. Windows uses the
|
The Settings tab calls the platform autostart implementation. Windows uses a
|
||||||
current user's Run registry key. Linux uses a desktop-session startup entry.
|
shortcut in the current user's Startup folder. Linux uses a desktop-session
|
||||||
|
startup entry. Both autostart mechanisms pass `--start-in-tray`, so the
|
||||||
|
scheduler starts without opening the main window after sign-in.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
IDI_ICON1 ICON "assets/pysentry.ico"
|
IDI_ICON1 ICON "assets/gosentry.ico"
|
||||||
|
|||||||
@@ -30,11 +30,11 @@ func SetAutostart(enabled bool, executablePath string, iconPath string) error {
|
|||||||
Type=Application
|
Type=Application
|
||||||
Name=PySentry
|
Name=PySentry
|
||||||
Comment=PySentry desktop scheduler
|
Comment=PySentry desktop scheduler
|
||||||
Exec=%s
|
Exec=%s %s
|
||||||
%s
|
%s
|
||||||
Terminal=false
|
Terminal=false
|
||||||
X-GNOME-Autostart-enabled=true
|
X-GNOME-Autostart-enabled=true
|
||||||
`, quoteDesktopExec(executablePath), desktopIconLine(iconPath))
|
`, quoteDesktopExec(executablePath), StartInTrayArgument, desktopIconLine(iconPath))
|
||||||
return os.WriteFile(desktopPath, []byte(desktopFile), 0o644)
|
return os.WriteFile(desktopPath, []byte(desktopFile), 0o644)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +63,8 @@ func AutostartStatus(expectedEnabled bool, executablePath string) (bool, string)
|
|||||||
if readErr != nil {
|
if readErr != nil {
|
||||||
return false, "Autostart desktop entry is missing"
|
return false, "Autostart desktop entry is missing"
|
||||||
}
|
}
|
||||||
if !strings.Contains(string(data), "Exec="+quoteDesktopExec(executablePath)) {
|
expectedExec := "Exec=" + quoteDesktopExec(executablePath) + " " + StartInTrayArgument
|
||||||
|
if !strings.Contains(string(data), expectedExec) {
|
||||||
return false, "Autostart desktop entry points to another executable"
|
return false, "Autostart desktop entry points to another executable"
|
||||||
}
|
}
|
||||||
return true, "Autostart is configured"
|
return true, "Autostart is configured"
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLinuxAutostartStartsInTray(t *testing.T) {
|
||||||
|
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
|
||||||
|
|
||||||
|
executablePath := "/opt/Go Sentry/gosentry"
|
||||||
|
if err := SetAutostart(true, executablePath, "/opt/Go Sentry/gosentry.png"); err != nil {
|
||||||
|
t.Fatalf("enable autostart: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
desktopPath, err := autostartDesktopPath()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolve desktop path: %v", err)
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(desktopPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read desktop entry: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedExec := "Exec=" + quoteDesktopExec(executablePath) + " " + StartInTrayArgument
|
||||||
|
if !strings.Contains(string(data), expectedExec) {
|
||||||
|
t.Fatalf("desktop entry does not start in tray: %s", data)
|
||||||
|
}
|
||||||
|
}
|
||||||
+130
-28
@@ -1,57 +1,159 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const autostartName = "PySentry"
|
const autostartName = "GoSentry"
|
||||||
|
const legacyAutostartName = "PySentry"
|
||||||
|
const startupShortcutFile = autostartName + ".lnk"
|
||||||
|
|
||||||
func SetAutostart(enabled bool, executablePath string, iconPath string) error {
|
func SetAutostart(enabled bool, executablePath string, iconPath string) error {
|
||||||
if enabled {
|
if err := cleanupLegacyRegistryAutostart(); err != nil {
|
||||||
// Remove any stale entry first. This makes "uncheck, save, check, save"
|
return err
|
||||||
// and even a plain "check, save" repair an old path after the executable
|
|
||||||
// was moved or renamed for a new version.
|
|
||||||
deleteCommand := exec.Command("reg.exe", "delete", `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, "/v", autostartName, "/f")
|
|
||||||
configureHiddenWindow(deleteCommand)
|
|
||||||
_ = deleteCommand.Run()
|
|
||||||
|
|
||||||
command := exec.Command("reg.exe", "add", `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, "/v", autostartName, "/t", "REG_SZ", "/d", strconv.Quote(executablePath), "/f")
|
|
||||||
configureHiddenWindow(command)
|
|
||||||
return command.Run()
|
|
||||||
}
|
}
|
||||||
command := exec.Command("reg.exe", "delete", `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, "/v", autostartName, "/f")
|
|
||||||
configureHiddenWindow(command)
|
shortcutPath, err := startupShortcutPath()
|
||||||
_ = command.Run()
|
if err != nil {
|
||||||
return nil
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if enabled {
|
||||||
|
return createStartupShortcut(shortcutPath, executablePath, iconPath)
|
||||||
|
}
|
||||||
|
return removeIfExists(shortcutPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func AutostartStatus(expectedEnabled bool, executablePath string) (bool, string) {
|
func AutostartStatus(expectedEnabled bool, executablePath string) (bool, string) {
|
||||||
command := exec.Command("reg.exe", "query", `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, "/v", autostartName)
|
shortcutPath, err := startupShortcutPath()
|
||||||
configureHiddenWindow(command)
|
|
||||||
output, err := command.Output()
|
|
||||||
if !expectedEnabled {
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
return false, "Startup folder cannot be resolved"
|
||||||
|
}
|
||||||
|
_, statErr := os.Stat(shortcutPath)
|
||||||
|
if !expectedEnabled {
|
||||||
|
if os.IsNotExist(statErr) {
|
||||||
|
if legacyRegistryAutostartExists() {
|
||||||
|
return false, "Legacy registry autostart exists; save settings to repair"
|
||||||
|
}
|
||||||
return true, "Autostart is off"
|
return true, "Autostart is off"
|
||||||
}
|
}
|
||||||
return false, "Autostart entry exists while setting is off"
|
if statErr != nil {
|
||||||
|
return false, "Autostart shortcut cannot be checked"
|
||||||
}
|
}
|
||||||
if err != nil {
|
return false, "Autostart shortcut exists while setting is off"
|
||||||
return false, "Autostart entry is missing"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
actual, ok := parseRegistryRunValue(string(output))
|
if os.IsNotExist(statErr) {
|
||||||
if !ok {
|
if legacyRegistryAutostartExists() {
|
||||||
return false, "Autostart entry cannot be read"
|
return false, "Legacy registry autostart exists; save settings to repair"
|
||||||
|
}
|
||||||
|
return false, "Autostart shortcut is missing"
|
||||||
|
}
|
||||||
|
if statErr != nil {
|
||||||
|
return false, "Autostart shortcut cannot be checked"
|
||||||
|
}
|
||||||
|
|
||||||
|
actual, arguments, err := readShortcut(shortcutPath)
|
||||||
|
if err != nil {
|
||||||
|
return false, "Autostart shortcut cannot be read"
|
||||||
}
|
}
|
||||||
if !sameWindowsPath(actual, executablePath) {
|
if !sameWindowsPath(actual, executablePath) {
|
||||||
return false, "Autostart points to another executable"
|
return false, "Autostart shortcut points to another executable"
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(arguments) != StartInTrayArgument {
|
||||||
|
return false, "Autostart shortcut does not start in tray"
|
||||||
}
|
}
|
||||||
return true, "Autostart is configured"
|
return true, "Autostart is configured"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func startupShortcutPath() (string, error) {
|
||||||
|
appData := os.Getenv("APPDATA")
|
||||||
|
if appData == "" {
|
||||||
|
return "", fmt.Errorf("APPDATA is not set")
|
||||||
|
}
|
||||||
|
return filepath.Join(appData, "Microsoft", "Windows", "Start Menu", "Programs", "Startup", startupShortcutFile), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createStartupShortcut(shortcutPath string, executablePath string, iconPath string) error {
|
||||||
|
if err := os.MkdirAll(filepath.Dir(shortcutPath), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
workingDirectory := filepath.Dir(executablePath)
|
||||||
|
if iconPath == "" {
|
||||||
|
iconPath = executablePath
|
||||||
|
}
|
||||||
|
script := `$shell = New-Object -ComObject WScript.Shell; $shortcut = $shell.CreateShortcut($env:GOSENTRY_SHORTCUT_PATH); $shortcut.TargetPath = $env:GOSENTRY_TARGET_PATH; $shortcut.Arguments = $env:GOSENTRY_ARGUMENTS; $shortcut.WorkingDirectory = $env:GOSENTRY_WORKING_DIRECTORY; $shortcut.IconLocation = $env:GOSENTRY_ICON_PATH; $shortcut.Save()`
|
||||||
|
command := exec.Command("powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script)
|
||||||
|
command.Env = append(os.Environ(),
|
||||||
|
"GOSENTRY_SHORTCUT_PATH="+shortcutPath,
|
||||||
|
"GOSENTRY_TARGET_PATH="+executablePath,
|
||||||
|
"GOSENTRY_ARGUMENTS="+StartInTrayArgument,
|
||||||
|
"GOSENTRY_WORKING_DIRECTORY="+workingDirectory,
|
||||||
|
"GOSENTRY_ICON_PATH="+iconPath,
|
||||||
|
)
|
||||||
|
configureHiddenWindow(command)
|
||||||
|
if output, err := command.CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("create startup shortcut: %w: %s", err, strings.TrimSpace(string(output)))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readShortcut(shortcutPath string) (string, string, error) {
|
||||||
|
script := `$shell = New-Object -ComObject WScript.Shell; $shortcut = $shell.CreateShortcut($env:GOSENTRY_SHORTCUT_PATH); [Console]::Out.Write($shortcut.TargetPath + [Environment]::NewLine + $shortcut.Arguments)`
|
||||||
|
command := exec.Command("powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script)
|
||||||
|
command.Env = append(os.Environ(), "GOSENTRY_SHORTCUT_PATH="+shortcutPath)
|
||||||
|
configureHiddenWindow(command)
|
||||||
|
output, err := command.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("read startup shortcut: %w: %s", err, strings.TrimSpace(string(output)))
|
||||||
|
}
|
||||||
|
lines := strings.SplitN(string(output), "\n", 2)
|
||||||
|
target := strings.TrimSpace(lines[0])
|
||||||
|
arguments := ""
|
||||||
|
if len(lines) > 1 {
|
||||||
|
arguments = strings.TrimSpace(lines[1])
|
||||||
|
}
|
||||||
|
return target, arguments, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readShortcutTarget(shortcutPath string) (string, error) {
|
||||||
|
target, _, err := readShortcut(shortcutPath)
|
||||||
|
return target, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeIfExists(path string) error {
|
||||||
|
err := os.Remove(path)
|
||||||
|
if err == nil || os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanupLegacyRegistryAutostart() error {
|
||||||
|
for _, name := range []string{legacyAutostartName, autostartName} {
|
||||||
|
command := exec.Command("reg.exe", "delete", `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, "/v", name, "/f")
|
||||||
|
configureHiddenWindow(command)
|
||||||
|
_ = command.Run()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func legacyRegistryAutostartExists() bool {
|
||||||
|
for _, name := range []string{legacyAutostartName, autostartName} {
|
||||||
|
command := exec.Command("reg.exe", "query", `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, "/v", name)
|
||||||
|
configureHiddenWindow(command)
|
||||||
|
if command.Run() == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func parseRegistryRunValue(output string) (string, bool) {
|
func parseRegistryRunValue(output string) (string, bool) {
|
||||||
for _, line := range strings.Split(output, "\n") {
|
for _, line := range strings.Split(output, "\n") {
|
||||||
fields := strings.Fields(strings.TrimSpace(line))
|
fields := strings.Fields(strings.TrimSpace(line))
|
||||||
|
|||||||
@@ -2,7 +2,11 @@
|
|||||||
|
|
||||||
package core
|
package core
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func TestParseRegistryRunValue(t *testing.T) {
|
func TestParseRegistryRunValue(t *testing.T) {
|
||||||
output := `
|
output := `
|
||||||
@@ -23,3 +27,50 @@ func TestSameWindowsPathIgnoresCaseAndQuotes(t *testing.T) {
|
|||||||
t.Fatal("expected paths to match")
|
t.Fatal("expected paths to match")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSameWindowsPathHandlesSpaces(t *testing.T) {
|
||||||
|
if !sameWindowsPath(`"D:\Local Git\GoSentry\gosentry.exe"`, `d:\local git\gosentry\gosentry.exe`) {
|
||||||
|
t.Fatal("expected paths with spaces to match")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStartupShortcutPathUsesUserStartupFolder(t *testing.T) {
|
||||||
|
t.Setenv("APPDATA", `C:\Users\mixem\AppData\Roaming`)
|
||||||
|
|
||||||
|
path, err := startupShortcutPath()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := `C:\Users\mixem\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\GoSentry.lnk`
|
||||||
|
if path != expected {
|
||||||
|
t.Fatalf("unexpected shortcut path: %q", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateStartupShortcutHandlesSpaces(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
shortcutPath := filepath.Join(tempDir, "GoSentry test.lnk")
|
||||||
|
targetPath := filepath.Join(tempDir, "Program Files", "GoSentry", "gosentry.exe")
|
||||||
|
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
|
||||||
|
t.Fatalf("create target directory: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(targetPath, []byte("test"), 0644); err != nil {
|
||||||
|
t.Fatalf("create target file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := createStartupShortcut(shortcutPath, targetPath, ""); err != nil {
|
||||||
|
t.Fatalf("create shortcut: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual, arguments, err := readShortcut(shortcutPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read shortcut: %v", err)
|
||||||
|
}
|
||||||
|
if !sameWindowsPath(actual, targetPath) {
|
||||||
|
t.Fatalf("shortcut target mismatch: got %q want %q", actual, targetPath)
|
||||||
|
}
|
||||||
|
if arguments != StartInTrayArgument {
|
||||||
|
t.Fatalf("shortcut arguments mismatch: got %q want %q", arguments, StartInTrayArgument)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ package core
|
|||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
|
// StartInTrayArgument is written to the Windows Startup shortcut so autostart
|
||||||
|
// can keep the scheduler running without flashing the main window. Manual
|
||||||
|
// 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 pysentry.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.
|
||||||
|
|||||||
+25
-9
@@ -42,9 +42,9 @@ const singleInstanceShowCommand = "show"
|
|||||||
type job = core.Job
|
type job = core.Job
|
||||||
type event = core.RunRecord
|
type event = core.RunRecord
|
||||||
|
|
||||||
func Run() {
|
func Run(startInTray bool) {
|
||||||
started := time.Now()
|
started := time.Now()
|
||||||
instanceListener, primary := acquireSingleInstance()
|
instanceListener, primary := acquireSingleInstance(!startInTray)
|
||||||
if !primary {
|
if !primary {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -62,6 +62,10 @@ func Run() {
|
|||||||
w.Resize(fyne.NewSize(1120, 720))
|
w.Resize(fyne.NewSize(1120, 720))
|
||||||
w.SetContent(newMainView(w, started))
|
w.SetContent(newMainView(w, started))
|
||||||
serveSingleInstance(instanceListener, w)
|
serveSingleInstance(instanceListener, w)
|
||||||
|
if startInTray {
|
||||||
|
a.Run()
|
||||||
|
return
|
||||||
|
}
|
||||||
w.ShowAndRun()
|
w.ShowAndRun()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +100,7 @@ func configureSystemTray(a fyne.App, w fyne.Window) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func acquireSingleInstance() (net.Listener, bool) {
|
func acquireSingleInstance(showExisting bool) (net.Listener, bool) {
|
||||||
listener, err := net.Listen("tcp", singleInstanceAddress)
|
listener, err := net.Listen("tcp", singleInstanceAddress)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return listener, true
|
return listener, true
|
||||||
@@ -104,7 +108,9 @@ func acquireSingleInstance() (net.Listener, bool) {
|
|||||||
|
|
||||||
connection, dialErr := net.DialTimeout("tcp", singleInstanceAddress, time.Second)
|
connection, dialErr := net.DialTimeout("tcp", singleInstanceAddress, time.Second)
|
||||||
if dialErr == nil {
|
if dialErr == nil {
|
||||||
|
if showExisting {
|
||||||
_, _ = io.WriteString(connection, singleInstanceShowCommand)
|
_, _ = io.WriteString(connection, singleInstanceShowCommand)
|
||||||
|
}
|
||||||
_ = connection.Close()
|
_ = connection.Close()
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
@@ -157,12 +163,13 @@ func newMainView(w fyne.Window, started time.Time) fyne.CanvasObject {
|
|||||||
schedulerPaused := false
|
schedulerPaused := false
|
||||||
filteredJobs := filteredJobIndexes(jobs, selectedFolder)
|
filteredJobs := filteredJobIndexes(jobs, selectedFolder)
|
||||||
title := widget.NewLabelWithStyle(jobs[selected].Name, fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
|
title := widget.NewLabelWithStyle(jobs[selected].Name, fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
|
||||||
folder := widget.NewLabel(jobs[selected].Folder)
|
title.Wrapping = fyne.TextWrapBreak
|
||||||
schedule := widget.NewLabel(jobs[selected].Schedule)
|
folder := newJobDetailLabel(jobs[selected].Folder)
|
||||||
command := widget.NewLabel(jobs[selected].Command)
|
schedule := newJobDetailLabel(jobs[selected].Schedule)
|
||||||
lastRun := widget.NewLabel(jobs[selected].LastRun)
|
command := newJobDetailLabel(jobs[selected].Command)
|
||||||
nextRun := widget.NewLabel(jobs[selected].NextRun)
|
lastRun := newJobDetailLabel(jobs[selected].LastRun)
|
||||||
state := widget.NewLabel(jobs[selected].LastState)
|
nextRun := newJobDetailLabel(jobs[selected].NextRun)
|
||||||
|
state := newJobDetailLabel(jobs[selected].LastState)
|
||||||
schedulerState := widget.NewLabel("Scheduler running")
|
schedulerState := widget.NewLabel("Scheduler running")
|
||||||
commandOutput := widget.NewTextGrid()
|
commandOutput := widget.NewTextGrid()
|
||||||
commandOutput.SetText(jobs[selected].Output)
|
commandOutput.SetText(jobs[selected].Output)
|
||||||
@@ -569,6 +576,15 @@ func detailRow(label string, value fyne.CanvasObject) fyne.CanvasObject {
|
|||||||
return container.NewGridWithColumns(2, caption, value)
|
return container.NewGridWithColumns(2, caption, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newJobDetailLabel(text string) *widget.Label {
|
||||||
|
label := widget.NewLabel(text)
|
||||||
|
// Job names, commands, and paths can be much wider than the details panel.
|
||||||
|
// Breaking long runs of text keeps Label.MinSize stable when the selection
|
||||||
|
// changes, so the right panel does not force the whole window to resize.
|
||||||
|
label.Wrapping = fyne.TextWrapBreak
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
|
||||||
func settingsRow(label string, value fyne.CanvasObject) fyne.CanvasObject {
|
func settingsRow(label string, value fyne.CanvasObject) fyne.CanvasObject {
|
||||||
caption := widget.NewLabelWithStyle(label, fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
|
caption := widget.NewLabelWithStyle(label, fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
|
||||||
caption.Wrapping = fyne.TextTruncate
|
caption.Wrapping = fyne.TextTruncate
|
||||||
|
|||||||
Reference in New Issue
Block a user