Skip to content

feat: adder-tray scaffolding#608

Merged
wolf31o2 merged 1 commit intomainfrom
feature/tray-scaffolding
Feb 9, 2026
Merged

feat: adder-tray scaffolding#608
wolf31o2 merged 1 commit intomainfrom
feature/tray-scaffolding

Conversation

@wolf31o2
Copy link
Copy Markdown
Member

@wolf31o2 wolf31o2 commented Feb 7, 2026


Summary by cubic

Add a cross-platform system tray app (adder-tray) to start, stop, and restart the adder process. Supports auto-start and graceful shutdown, with config, platform paths, a process manager, tests, and a dedicated build target.

  • New Features

    • Tray menu via fyne.io/systray: Start/Stop/Restart/Quit; handles SIGINT/SIGTERM for clean exit.
    • YAML config (adder-tray.yaml): adder_binary, adder_config, auto_start; platform config/log dirs; load/save with tests.
    • Process manager: runs adder (optional --config), streams stdout/stderr, monitors exit, graceful stop (10s), IsRunning/Restart.
  • Migration

    • Build with: make build-tray (not in default build). CGO required on macOS; pure Go on Linux/Windows.
    • Optional config: place adder-tray.yaml in the config dir (Linux: ~/.config/adder, macOS: ~/Library/Application Support/Adder, Windows: %APPDATA%\Adder).

Written for commit f492948. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Adds a system-tray application to control the adder process (Start/Stop/Restart/Quit), with optional auto-start and cross-platform path handling.
    • Introduces a process manager to launch, monitor, and gracefully stop the adder subprocess.
    • Adds a dedicated build target to produce the tray binary.
  • Tests

    • Adds unit tests for configuration handling and platform-specific path utilities.
  • Chores

    • Updates dependencies and ignores the tray artifact in VCS.

@wolf31o2 wolf31o2 requested a review from a team as a code owner February 7, 2026 14:35
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 7, 2026

📝 Walkthrough

Walkthrough

Adds a new system-tray application and supporting package tray with cross-platform path helpers, configuration (load/save), and a ProcessManager to start/stop/restart the adder subprocess. Introduces cmd/adder-tray/main.go as a new entry point and a build-tray Makefile target while excluding adder-tray from the default BINARIES. Updates go.mod to add direct dependencies. Adds unit tests for config and path utilities and updates .gitignore to ignore /adder-tray.

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: adder-tray scaffolding' clearly and concisely describes the main change: introducing the foundational implementation of a new system tray application component.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/tray-scaffolding

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 14 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="tray/paths_darwin.go">

<violation number="1" location="tray/paths_darwin.go:25">
P2: Ignoring the error from os.UserHomeDir can yield an empty home path and cause config/logs to be written to a relative directory. Handle the error explicitly to avoid misdirected writes.</violation>
</file>

<file name="tray/paths_test.go">

<violation number="1" location="tray/paths_test.go:37">
P2: The substring check is case-sensitive. On macOS/Windows the implementation uses "Adder" with a capital A, so this assertion will fail on those platforms. Use a case-insensitive comparison (or normalize the path) before checking for the project name.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

)

func configDir() string {
home, _ := os.UserHomeDir()
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Ignoring the error from os.UserHomeDir can yield an empty home path and cause config/logs to be written to a relative directory. Handle the error explicitly to avoid misdirected writes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tray/paths_darwin.go, line 25:

<comment>Ignoring the error from os.UserHomeDir can yield an empty home path and cause config/logs to be written to a relative directory. Handle the error explicitly to avoid misdirected writes.</comment>

<file context>
@@ -0,0 +1,32 @@
+)
+
+func configDir() string {
+	home, _ := os.UserHomeDir()
+	return filepath.Join(home, "Library", "Application Support", "Adder")
+}
</file context>
Fix with Cubic

assert.Contains(
t,
dir,
"adder",
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The substring check is case-sensitive. On macOS/Windows the implementation uses "Adder" with a capital A, so this assertion will fail on those platforms. Use a case-insensitive comparison (or normalize the path) before checking for the project name.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tray/paths_test.go, line 37:

<comment>The substring check is case-sensitive. On macOS/Windows the implementation uses "Adder" with a capital A, so this assertion will fail on those platforms. Use a case-insensitive comparison (or normalize the path) before checking for the project name.</comment>

<file context>
@@ -0,0 +1,73 @@
+	assert.Contains(
+		t,
+		dir,
+		"adder",
+		"ConfigDir should contain 'adder' in the path",
+	)
</file context>
Fix with Cubic

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In `@Makefile`:
- Around line 42-49: The default build target is picking up adder-tray from
BINARIES (auto-discovered under cmd/) and building it with incompatible flags
(CGO_ENABLED=0 and -tags nodbus), causing broken tray builds; update BINARIES to
exclude the adder-tray directory (so the default build target won't build it)
and leave the specialized build-tray target (function/target: build-tray, binary
name: adder-tray) to handle platform-specific CGO/flags, e.g., adjust the
BINARIES definition to filter out "adder-tray" so only build-tray produces that
binary with the correct CGO and tag settings.

In `@tray/config_test.go`:
- Around line 49-59: Tests like
TestLoadConfigMissing/TestSaveAndLoadConfig/TestConfigExists rely on
XDG_CONFIG_HOME which only affects Linux paths; to make them platform-agnostic
add a test-time override for the config directory (e.g., function
setConfigDirOverride(path string) and clearConfigDirOverride()) and update the
path resolution used by LoadConfig, SaveConfig and ConfigExists to consult that
override before falling back to os.UserHomeDir()/platform-specific logic; then
call setConfigDirOverride(tmpDir) at the start of each test (and clear it after)
so tests run safely on macOS/Windows/Linux without modifying platform env vars.

In `@tray/paths_linux.go`:
- Around line 24-30: The configDir() function (and its counterpart in
paths_darwin.go) currently ignores the error from os.UserHomeDir(), which can
yield an empty string and produce a relative path; update configDir to return
(string, error) instead of string, check and propagate the os.UserHomeDir()
error (or if you prefer a safe fallback use a well-known absolute path like
"/tmp/adder" explicitly), and update callers such as SaveConfig to handle the
error return and avoid creating files relative to CWD; ensure the new signature
is applied to both configDir and any internal callers to keep behavior
consistent.

In `@tray/paths_test.go`:
- Around line 34-39: The tests TestConfigDir and TestLogDir use
assert.Contains(t, dir, "adder") which is case-sensitive and fails on Windows
where the path contains "Adder"; change the assertions to perform a
case-insensitive containment check (e.g. assert.True(t,
strings.Contains(strings.ToLower(dir), "adder"), ...) and update the error
message accordingly, and add the strings import if missing.

In `@tray/paths_windows.go`:
- Around line 28-29: The code silently ignores os.UserHomeDir() errors (used
with filepath.Join) which can produce a relative path; change the helper to
check the error from os.UserHomeDir() and handle it explicitly: if
os.UserHomeDir() returns an error, try using os.Getenv("APPDATA") as a fallback
(and verify it's non-empty), otherwise return an error (i.e., change the helper
signatures to return (string, error)) or at minimum log a warning before
returning; apply the same fix to logDir(), update callers to handle the new
error return or fallback behavior, and ensure any returned path is absolute (use
filepath.Abs if needed).

In `@tray/process.go`:
- Around line 128-133: Restart() races because Stop() only signals the process
and returns before the background goroutine clears pm.cmd, so Start() often sees
pm.cmd non-nil and errors; fix by making Stop() block until the process actually
exits (call pm.cmd.Wait() while holding appropriate locks or wait on an
existing/added exit/done channel that the background goroutine closes) so pm.cmd
is cleared before Stop() returns, or alternatively change Restart() to
wait/retry until pm.cmd is nil after calling Stop() (use a short loop with
timeout). Update the implementations of Stop(), Start(), and Restart() (and the
pm.cmd/exit signaling mechanism) to coordinate via the chosen wait strategy to
avoid the race.
- Around line 120-122: The Stop() logic uses pm.cmd.Process.Signal(os.Interrupt)
which is not implemented on Windows; update Stop() to detect Windows
(runtime.GOOS == "windows") or handle the specific error from
pm.cmd.Process.Signal and call pm.cmd.Process.Kill() on Windows (or as a
fallback) instead of returning an error; modify the code paths around
pm.cmd.Process.Signal, pm.cmd.Process.Kill, and Stop() so Windows builds use
Kill() for termination while other platforms continue to use
Signal(os.Interrupt).
🧹 Nitpick comments (5)
cmd/adder-tray/main.go (1)

37-41: Consider handling a second signal for forceful exit.

Currently the goroutine exits after consuming the first signal. If application.Shutdown() or Run() hangs, there's no way for the user to force-quit with a second Ctrl+C. A common pattern is to loop and os.Exit(1) on the second signal.

💡 Optional: force-exit on second signal
 	go func() {
 		<-sigChan
 		slog.Info("received shutdown signal")
 		application.Shutdown()
+		<-sigChan
+		slog.Warn("received second signal, forcing exit")
+		os.Exit(1)
 	}()
tray/config_test.go (1)

61-81: Verify round-trip preserves zero-value fields.

The test uses all non-zero values. Consider adding a case where AdderConfig is "" and AutoStart is false to ensure YAML round-tripping preserves zero values correctly (some YAML libraries omit zero-value fields by default unless omitempty is absent).

tray/paths_test.go (1)

58-72: Platform test for Windows is weak — "Adder" also appears on Linux/macOS paths.

The Windows branch only checks that the path contains "Adder", which is true for all platforms. Consider asserting a Windows-specific marker like "AppData" to validate the platform-appropriate path.

tray/app.go (1)

31-50: NewApp never returns a non-nil error.

The current implementation always returns nil for the error. If this is intentional scaffolding for future error paths, that's fine — but consider documenting it or simplifying the signature to just *App until an error path actually exists.

tray/process.go (1)

94-104: Background goroutine holds the lock while the main Stop path also needs it.

This is fine for the current flow since Stop() returns before Wait() completes, and the goroutine acquires the lock after. However, note that if Stop() were ever changed to wait for exit while holding the lock, this would deadlock. The current design is correct but fragile — worth a comment noting the lock ordering.

Comment on lines +49 to +59
func TestLoadConfigMissing(t *testing.T) {
// When no config file exists, LoadConfig should return defaults
// without error. We set XDG_CONFIG_HOME to a temp dir so we
// don't accidentally read a real config.
tmpDir := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", tmpDir)

cfg, err := LoadConfig()
require.NoError(t, err)
assert.Equal(t, DefaultConfig(), cfg)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Test isolation via XDG_CONFIG_HOME only works on Linux.

t.Setenv("XDG_CONFIG_HOME", tmpDir) redirects config paths on Linux, but paths_darwin.go and paths_windows.go ignore this variable entirely — they use os.UserHomeDir() and platform-specific paths. On macOS/Windows, these tests will read from and write to the developer's real config directory, which is both fragile and potentially destructive.

Consider one of:

  1. Adding a //go:build linux constraint to this test file (limits coverage).
  2. Introducing an internal override/setter (e.g., setConfigDirOverride(path)) for test-time path redirection on all platforms.
  3. Using t.Setenv("HOME", tmpDir) on Darwin (and APPDATA/LOCALAPPDATA on Windows) to redirect os.UserHomeDir() — though this is more brittle.

The same concern applies to TestSaveAndLoadConfig and TestConfigExists.

🤖 Prompt for AI Agents
In `@tray/config_test.go` around lines 49 - 59, Tests like
TestLoadConfigMissing/TestSaveAndLoadConfig/TestConfigExists rely on
XDG_CONFIG_HOME which only affects Linux paths; to make them platform-agnostic
add a test-time override for the config directory (e.g., function
setConfigDirOverride(path string) and clearConfigDirOverride()) and update the
path resolution used by LoadConfig, SaveConfig and ConfigExists to consult that
override before falling back to os.UserHomeDir()/platform-specific logic; then
call setConfigDirOverride(tmpDir) at the start of each test (and clear it after)
so tests run safely on macOS/Windows/Linux without modifying platform env vars.

@wolf31o2 wolf31o2 force-pushed the feature/tray-scaffolding branch from 76b62fd to 45a1cb0 Compare February 7, 2026 15:01
@wolf31o2
Copy link
Copy Markdown
Member Author

wolf31o2 commented Feb 7, 2026

@cubic-dev-ai review

@cubic-dev-ai
Copy link
Copy Markdown

cubic-dev-ai bot commented Feb 7, 2026

@cubic-dev-ai review

@wolf31o2 I have started the AI code review. It will take a few minutes to complete.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@tray/process.go`:
- Around line 114-144: The Stop method may attempt to signal a process that
already exited; update ProcessManager.Stop to detect that case and treat it as
success: before sending a signal, check pm.cmd.ProcessState (via
pm.cmd.ProcessState != nil && pm.cmd.ProcessState.Exited()) and return nil if
the process is already finished; additionally, when Signal or Kill returns an
error, treat "process already finished" (or platform-specific equivalent) as a
no-op instead of returning an error (e.g., inspect the error string from
pm.cmd.Process.Signal / pm.cmd.Process.Kill and swallow the already-finished
error). Ensure you still unlock pm.mu in all paths and keep using pm.done to
wait when appropriate.
🧹 Nitpick comments (2)
tray/process.go (1)

114-144: Stop() has repetitive unlock-and-return paths — consider defer with a flag or restructuring.

The manual pm.mu.Unlock() calls on multiple error branches are fragile and easy to get wrong during future edits. Consider restructuring to reduce duplication, e.g., unlock before signaling (the done channel already ensures coordination), or capture the error and defer the unlock.

Suggested restructuring
 func (pm *ProcessManager) Stop() error {
 	pm.mu.Lock()
 
 	if pm.cmd == nil || pm.cmd.Process == nil {
 		pm.mu.Unlock()
 		return nil
 	}
 
 	slog.Info("stopping adder process", "pid", pm.cmd.Process.Pid)
 
 	done := pm.done
+	proc := pm.cmd.Process
+	pm.mu.Unlock()
 
+	var err error
 	if runtime.GOOS == "windows" {
-		// os.Interrupt is not supported on Windows
-		if err := pm.cmd.Process.Kill(); err != nil {
-			pm.mu.Unlock()
-			return fmt.Errorf("killing adder: %w", err)
-		}
+		err = proc.Kill()
 	} else {
-		if err := pm.cmd.Process.Signal(os.Interrupt); err != nil {
-			pm.mu.Unlock()
-			return fmt.Errorf("sending interrupt to adder: %w", err)
-		}
+		err = proc.Signal(os.Interrupt)
 	}
 
-	pm.mu.Unlock()
+	if err != nil && !errors.Is(err, os.ErrProcessDone) {
+		return fmt.Errorf("stopping adder: %w", err)
+	}
 
-	// Wait for the process to fully exit
 	<-done
-
 	return nil
 }
tray/app.go (1)

31-49: NewApp never returns a non-nil error.

NewApp currently always returns (app, nil). The error return is unused since config loading failures are handled with a fallback. If this is intentional scaffolding for future validation, it's fine to keep — just noting the current state.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 14 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="tray/process.go">

<violation number="1" location="tray/process.go:142">
P2: Stop() can block forever if the adder process doesn't exit after SIGINT. Add a timeout and force-kill fallback so the tray UI can't hang indefinitely.</violation>
</file>

<file name="tray/paths_test.go">

<violation number="1" location="tray/paths_test.go:63">
P2: The linux test assumes .config/.local are always present, but the implementation respects XDG_CONFIG_HOME/XDG_STATE_HOME. This makes the test environment-dependent and can fail when those env vars are set. Consider asserting against the env-provided path when set, and falling back to the default paths otherwise.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

tray/process.go Outdated
pm.mu.Unlock()

// Wait for the process to fully exit
<-done
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Stop() can block forever if the adder process doesn't exit after SIGINT. Add a timeout and force-kill fallback so the tray UI can't hang indefinitely.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tray/process.go, line 142:

<comment>Stop() can block forever if the adder process doesn't exit after SIGINT. Add a timeout and force-kill fallback so the tray UI can't hang indefinitely.</comment>

<file context>
@@ -0,0 +1,161 @@
+	pm.mu.Unlock()
+
+	// Wait for the process to fully exit
+	<-done
+
+	return nil
</file context>
Fix with Cubic


switch runtime.GOOS {
case "linux":
assert.Contains(t, configDir, ".config")
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The linux test assumes .config/.local are always present, but the implementation respects XDG_CONFIG_HOME/XDG_STATE_HOME. This makes the test environment-dependent and can fail when those env vars are set. Consider asserting against the env-provided path when set, and falling back to the default paths otherwise.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tray/paths_test.go, line 63:

<comment>The linux test assumes .config/.local are always present, but the implementation respects XDG_CONFIG_HOME/XDG_STATE_HOME. This makes the test environment-dependent and can fail when those env vars are set. Consider asserting against the env-provided path when set, and falling back to the default paths otherwise.</comment>

<file context>
@@ -0,0 +1,72 @@
+
+	switch runtime.GOOS {
+	case "linux":
+		assert.Contains(t, configDir, ".config")
+		assert.Contains(t, logDir, ".local")
+	case "darwin":
</file context>
Fix with Cubic

@wolf31o2 wolf31o2 force-pushed the feature/tray-scaffolding branch from 45a1cb0 to b007ca9 Compare February 7, 2026 15:10
@wolf31o2
Copy link
Copy Markdown
Member Author

wolf31o2 commented Feb 7, 2026

@cubic-dev-ai review

@cubic-dev-ai
Copy link
Copy Markdown

cubic-dev-ai bot commented Feb 7, 2026

@cubic-dev-ai review

@wolf31o2 I have started the AI code review. It will take a few minutes to complete.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 14 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="tray/process.go">

<violation number="1" location="tray/process.go:162">
P2: Ignoring the force-kill error can leave Stop() waiting on `done` forever if the process could not be killed. Handle the error and return instead of unconditionally blocking.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

tray/process.go Outdated
slog.Warn("adder process did not exit in time, force killing")
pm.mu.Lock()
if pm.cmd != nil && pm.cmd.Process != nil {
_ = pm.cmd.Process.Kill()
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Ignoring the force-kill error can leave Stop() waiting on done forever if the process could not be killed. Handle the error and return instead of unconditionally blocking.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tray/process.go, line 162:

<comment>Ignoring the force-kill error can leave Stop() waiting on `done` forever if the process could not be killed. Handle the error and return instead of unconditionally blocking.</comment>

<file context>
@@ -0,0 +1,185 @@
+		slog.Warn("adder process did not exit in time, force killing")
+		pm.mu.Lock()
+		if pm.cmd != nil && pm.cmd.Process != nil {
+			_ = pm.cmd.Process.Kill()
+		}
+		pm.mu.Unlock()
</file context>
Fix with Cubic

Signed-off-by: Chris Gianelloni <wolf31o2@blinklabs.io>
@wolf31o2 wolf31o2 force-pushed the feature/tray-scaffolding branch from b007ca9 to f492948 Compare February 7, 2026 15:55
@wolf31o2
Copy link
Copy Markdown
Member Author

wolf31o2 commented Feb 8, 2026

@cubic-dev-ai review

@cubic-dev-ai
Copy link
Copy Markdown

cubic-dev-ai bot commented Feb 8, 2026

@cubic-dev-ai review

@wolf31o2 I have started the AI code review. It will take a few minutes to complete.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 14 files

@wolf31o2 wolf31o2 merged commit ea3261a into main Feb 9, 2026
12 checks passed
@wolf31o2 wolf31o2 deleted the feature/tray-scaffolding branch February 9, 2026 13:51
This was referenced Feb 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants