Files
ReactorcoremeltdownandClaude Opus 5.5 ead82dc777 Turn the desktop client into a system tray application
Started without a command, keeporg now shows a padlock in the system tray
with a menu like midi-webhook's: a status line, the mirror folder, each
database with its state, Sync Now, browser sign-in and sign-out, the web
interface, the configuration, the log, Start at Login, About and Quit. The
commands are unchanged.

config.json now carries every client setting (device_label,
request_timeout_seconds, ca_file, log_file, notifications), is written in
full on first start and is reloaded by the tray when saved.

A lock in the mirror directory admits one agent at a time, since the tray,
`keeporg run` and a service unit could otherwise race each other.

The tray uses fyne.io/systray: pure Go on Linux, BSD and Windows, cgo on
macOS only. The server keeps no third-party dependencies.

Desktop versions go to 0.5.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 12:30:28 +01:00

33 lines
1.0 KiB
Go

package client
import (
"errors"
"fmt"
"os"
"path/filepath"
)
// lockFile marks a mirror directory as being synchronized by one agent. Two
// agents on one directory would each think the other's writes were local
// saves and push them back, so the tray, `keeporg run` and a service unit
// started side by side must not all sync.
const lockFile = ".keeporg.lock"
// ErrMirrorBusy means another agent already holds the mirror directory.
var ErrMirrorBusy = errors.New("another keeporg agent is already synchronizing this directory")
// LockDir takes the mirror directory's lock for this process. The operating
// system releases it when the process exits, however it exits, so a crash
// never leaves a directory locked.
func LockDir(dir string) (unlock func(), err error) {
if err := os.MkdirAll(dir, 0o700); err != nil {
return nil, err
}
path := filepath.Join(dir, lockFile)
unlock, err = lockPath(path)
if errors.Is(err, ErrMirrorBusy) {
return nil, fmt.Errorf("%s: %w", dir, err)
}
return unlock, err
}