Skip to content

feat(application): cross-platform Autostart manager - #5426

Merged
leaanthony merged 3 commits into
masterfrom
feat/autostart
May 13, 2026
Merged

feat(application): cross-platform Autostart manager#5426
leaanthony merged 3 commits into
masterfrom
feat/autostart

Conversation

@leaanthony

@leaanthony leaanthony commented May 13, 2026

Copy link
Copy Markdown
Member

Summary

Adds `app.Autostart` for registering the application to launch at user login on macOS, Windows, and Linux. Closes the long-standing gap in the manager API and supersedes the AppleScript-only approach floated in earlier drafts (#3910, #4474) — that approach triggered the macOS automation TCC prompt and didn't work for sandboxed/MAS apps.

if err := app.Autostart.Enable(); err != nil {
    if errors.Is(err, application.ErrAutostartNotSupported) {
        // hide the toggle in the UI
        return
    }
    // real failure
}

API

app.Autostart.Enable()                        // default options
app.Autostart.EnableWithOptions(opts)         // Identifier + Arguments
app.Autostart.Disable()                       // idempotent
app.Autostart.IsEnabled() (bool, error)       // fast — no path validation
app.Autostart.Status() (AutostartStatus, error) // Enabled + Path + Strategy

`AutostartOptions` has two fields: `Identifier` (overrides the auto-derived registration ID — registry value name on Windows, plist Label on macOS, `.desktop` filename on Linux) and `Arguments` (appended to the executable path on launch).

`AutostartStatus.Strategy` names the mechanism used so apps can present meaningful diagnostics (`smappservice`, `launchagent`, `registry-run`, `xdg-autostart`).

Per-platform mechanism

Platform Mechanism When
macOS 13+ bundled `.app` `SMAppService.mainAppService` First choice — works for sandboxed / Mac App Store apps, no TCC prompt
macOS pre-13 or unbundled `~/Library/LaunchAgents/.plist` + `launchctl bootstrap` Fallback for dev builds and pre-Ventura
Windows `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` value Standard user-scope autostart
Linux `$XDG_CONFIG_HOME/autostart/.desktop` freedesktop XDG spec
iOS / Android / server `ErrAutostartNotSupported`

Design notes

  • Atomic writes. `.plist` and `.desktop` files are written via tempfile + `os.Rename` so a partial write never leaves a half-formed file in place.
  • Symlink-aware. `os.Executable()` is resolved through `filepath.EvalSymlinks` so registrations survive symlinked installs (Homebrew, Scoop).
  • Stale detection by EXE path. `Disable()` and `Status()` locate the registration by matching the registered executable path against the current binary, not by identifier lookup. This means renaming the binary or changing the identifier between releases doesn't strand the registration.
  • Quote-correct arguments. Windows uses `CommandLineToArgvW`-style backslash-doubling for embedded quotes; Linux uses freedesktop `Exec=` escaping for backticks, dollars, and embedded quotes.
  • Test seams. `launchctlBootstrap` / `launchctlBootout` are package-level `var`s so tests can stub them out — without that, a test plist with `RunAtLoad=true` would respawn the test binary recursively via launchd. The Windows impl exposes its registry sub-key as a struct field for the same reason.

What's included

  • `v3/pkg/application/autostart.go` — shared types, helpers, validation
  • `v3/pkg/application/autostart_manager.go` — public API
  • `autostart_{darwin,linux,windows}.go` — platform implementations
  • `autostart_darwin_smappservice.go` — CGO bridge with `@available(macOS 13.0, *)` runtime gate
  • `autostart_{android,ios,server}.go` — stubs returning `ErrAutostartNotSupported`
  • `autostart_{,darwin_,linux_}test.go` — unit tests (slug / validate / atomic write / round-trip per OS)
  • `examples/autostart/main.go` — runnable status/enable/disable demo
  • `docs/features/autostart/basics.mdx` — feature reference
  • `docs/concepts/manager-api.mdx` — Autostart added to the manager list
  • `docs/astro.config.mjs` — sidebar entry
  • `application.go` — `App.Autostart` field + `newAutostartManager` wired in `init()`

Test plan

  • darwin: `go build`, `go test` — all autostart tests pass (round-trip via LaunchAgent, custom identifier, validation, disable noop, plist parser, runningFromAppBundle, slug)
  • linux (Ubuntu, GTK4 + WebKit2GTK-4.1 on lin-node1): `go build`, `go test` — pass
  • windows: `GOOS=windows go vet ./pkg/application/` — clean (existing pre-existing unsafe.Pointer warnings in unrelated files only)
  • manual: notarised `.app` on macOS 13+ to exercise the SMAppService runtime branch (not exercised by unit tests — needs a real bundle ID)
  • manual: confirm `HKCU\…\Run` entry appears + survives reboot on Windows
  • manual: confirm `.desktop` entry triggers a launch via `gnome-session` / KDE autostart on Linux

Closes

Resolves the `app.Autostart` slot in the manager API. Earlier closed drafts: #3910, #4474 (both AppleScript-only macOS, didn't work for sandboxed apps).

Summary by CodeRabbit

  • New Features

    • Added cross-platform Autostart manager to control app autostart (enable/disable/status) and exposed it on the application API; included a runnable example demonstrating usage.
  • Documentation

    • New comprehensive Autostart docs with platform details and a sidebar "Autostart" entry linked from Features and Manager API pages.
  • Tests

    • Added extensive unit tests for Autostart behavior on macOS, Windows, Linux, plus unsupported-target stubs.

Review Change Stack

Adds app.Autostart for registering the application to launch at user
login. Picks the right native mechanism per platform and falls back
cleanly when one is unavailable.

Mechanisms:
- macOS 13+ bundled .app: SMAppService.mainAppService — works for
  sandboxed / Mac App Store apps, no TCC automation prompt (the
  historical AppleScript approach triggered one)
- macOS older / unbundled: ~/Library/LaunchAgents/<id>.plist with
  RunAtLoad=true, activated via launchctl bootstrap
- Windows: HKCU\Software\Microsoft\Windows\CurrentVersion\Run, with
  CommandLineToArgvW-style quoting for paths with spaces
- Linux: $XDG_CONFIG_HOME/autostart/<id>.desktop with freedesktop-spec
  Exec= escaping
- Android / iOS / server: ErrAutostartNotSupported (use errors.Is)

API:
  app.Autostart.Enable()
  app.Autostart.EnableWithOptions(AutostartOptions{Identifier, Arguments})
  app.Autostart.Disable()
  app.Autostart.IsEnabled() (bool, error)
  app.Autostart.Status() (AutostartStatus, error)

Design notes:
- All on-disk artefacts (.plist, .desktop) are written via tempfile +
  os.Rename so a partial write never leaves a half-formed file.
- os.Executable() is resolved through filepath.EvalSymlinks so
  registrations survive symlink-based installs (Homebrew, Scoop).
- Disable() and Status() match the registered EXE path rather than
  the stored identifier, so renaming the binary or changing the
  identifier between releases doesn't strand the registration.
- Test seams (launchctlBootstrap/Bootout as package vars, Windows
  registry sub-key field) keep unit tests from accidentally launching
  the test binary on the host (a real risk with RunAtLoad=true).

Includes:
- examples/autostart/ — runnable status/enable/disable demo
- docs/features/autostart/basics.mdx — feature reference
- docs/concepts/manager-api.mdx — Autostart added to the manager list
- docs/astro.config.mjs — sidebar entry
Copilot AI review requested due to automatic review settings May 13, 2026 09:39
@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2f3c4101-0327-4213-81d8-cd96687d15a2

📥 Commits

Reviewing files that changed from the base of the PR and between 0d67b58 and 51eebc9.

📒 Files selected for processing (4)
  • v3/examples/autostart/main.go
  • v3/pkg/application/autostart_darwin.go
  • v3/pkg/application/autostart_linux.go
  • v3/pkg/application/autostart_windows.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • v3/examples/autostart/main.go
  • v3/pkg/application/autostart_darwin.go
  • v3/pkg/application/autostart_linux.go
  • v3/pkg/application/autostart_windows.go

Walkthrough

Adds a cross-platform AutostartManager and implementations for macOS (SMAppService + LaunchAgent), Windows (Registry Run), and Linux (XDG .desktop), plus docs, an example app, and unit tests; autostart options, status reporting, and identifier handling are exposed via the new API.

Changes

Autostart Feature Implementation

Layer / File(s) Summary
Documentation and Example Application
docs/astro.config.mjs, docs/src/content/docs/concepts/manager-api.mdx, docs/src/content/docs/features/autostart/basics.mdx, v3/examples/autostart/main.go
Sidebar navigation, API overview, comprehensive feature documentation covering platform behaviour, identifier derivation, stale detection by executable-path matching, and a runnable example app with menu-based autostart control.
Core API, Manager, and Shared Utilities
v3/pkg/application/autostart.go, v3/pkg/application/autostart_manager.go, v3/pkg/application/autostart_test.go
Public API types (AutostartOptions, AutostartStatus, AutostartStrategy), helpers (executable resolution, atomic write, identifier validation/slug), AutostartManager facade delegating to platform implementations, and tests for core helpers.
Application Integration
v3/pkg/application/application.go
Wires Autostart *AutostartManager into App and initializes it during app construction.
macOS Implementation (LaunchAgent)
v3/pkg/application/autostart_darwin.go, v3/pkg/application/autostart_darwin_test.go
LaunchAgent plist management, discovery, bootstrap helpers, plist XML generation/parsing, bundle/version detection, and tests validating enable/disable, custom identifiers/arguments, and parsing edge cases.
macOS SMAppService cgo wrapper
v3/pkg/application/autostart_darwin_smappservice.go
cgo bindings and Go wrappers for SMAppService register/unregister/status with availability and error mapping for macOS 13+ when applicable.
Windows Implementation (Registry Run)
v3/pkg/application/autostart_windows.go, v3/pkg/application/autostart_windows_test.go
Per-user Registry Run-key autostart: identifier validation, executable resolution, Windows-safe argument quoting/escaping, enable/disable via HKCU, status discovery by matching executable token, and tests for parsing/quoting helpers.
Linux Implementation (XDG Desktop Entries)
v3/pkg/application/autostart_linux.go, v3/pkg/application/autostart_linux_test.go
XDG autostart via .desktop files in ~/.config/autostart (respecting $XDG_CONFIG_HOME). Builds quoted Exec= tokens, writes atomic .desktop entries, discovers existing entries by parsing Exec=, and includes tests for round-trip enable/disable, custom identifiers, and desktop-entry parsing/quoting.
Unsupported Platform Stubs
v3/pkg/application/autostart_ios.go, v3/pkg/application/autostart_android.go, v3/pkg/application/autostart_server.go
Build-tagged stubs for iOS, Android, and server that consistently return ErrAutostartNotSupported.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Suggested labels

Documentation, size:XXL, lgtm

🐰 I found a slug and made a name,
Woven paths so apps can claim
A gentle startup, neat and kind,
Plists, registry, desktop aligned.
Hooray — autostart for humankind!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(application): cross-platform Autostart manager' clearly and concisely describes the main change—adding a cross-platform autostart feature to the application manager API.
Description check ✅ Passed The PR description comprehensively covers the summary, API, per-platform mechanisms, design notes, included files, test plan, and related issues, fulfilling all key template requirements despite some optional checklist items being incomplete.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/autostart

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds a new app.Autostart manager to the v3 application API, providing cross-platform enable/disable/status for launching the app at user login on macOS, Windows, and Linux, with platform stubs returning ErrAutostartNotSupported for unsupported targets.

Changes:

  • Introduces AutostartManager public API plus shared helpers/types (AutostartOptions, AutostartStatus, strategy enum).
  • Implements per-OS autostart registration (LaunchAgent/SMAppService on macOS, HKCU Run key on Windows, XDG .desktop on Linux) and adds unit tests (darwin/linux + shared helpers).
  • Adds an example app and documentation pages/sidebar entries for the new feature.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
v3/pkg/application/autostart.go Shared helpers (resolved executable, atomic writes), identifier validation, shared types/status/strategy.
v3/pkg/application/autostart_manager.go Public app.Autostart API surface (Enable/Disable/IsEnabled/Status).
v3/pkg/application/autostart_windows.go Windows implementation using HKCU\...\Run registry values + quoting/parsing helpers.
v3/pkg/application/autostart_linux.go Linux implementation writing XDG autostart .desktop files.
v3/pkg/application/autostart_darwin.go macOS implementation using SMAppService where available and LaunchAgent fallback.
v3/pkg/application/autostart_darwin_smappservice.go CGO bridge to SMAppService.mainAppService register/unregister/status.
v3/pkg/application/autostart_android.go Android stub returning ErrAutostartNotSupported.
v3/pkg/application/autostart_ios.go iOS stub returning ErrAutostartNotSupported.
v3/pkg/application/autostart_server.go Server build stub returning ErrAutostartNotSupported.
v3/pkg/application/autostart_test.go Unit tests for slugging, identifier validation, and atomic writes helper.
v3/pkg/application/autostart_linux_test.go Linux unit tests for round-trip and escaping/parsing helpers.
v3/pkg/application/autostart_darwin_test.go macOS unit tests for LaunchAgent round-trip + plist parsing helper.
v3/pkg/application/application.go Wires AutostartManager onto App and initializes it.
v3/examples/autostart/main.go Runnable demo showing enable/disable/status via menu actions.
docs/src/content/docs/features/autostart/basics.mdx New feature documentation page covering API + per-platform behavior.
docs/src/content/docs/concepts/manager-api.mdx Adds app.Autostart to the Manager API concept doc + usage snippet.
docs/astro.config.mjs Adds Autostart section to docs sidebar autogeneration.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread v3/pkg/application/autostart.go Outdated
Comment on lines +36 to +40
if _, err := tmp.Write(data); err != nil {
_ = tmp.Close()
cleanup()
return err
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 0d67b58. Added a defensive n == len(data) check after tmp.Write returning io.ErrShortWrite if they disagree. os.File.Write is documented to fail-fast on short writes so this is belt-and-braces, but it removes the silent-truncation risk if the writer type ever changes.

Comment thread v3/pkg/application/autostart.go Outdated
Comment on lines +125 to +134
// AutostartStrategy names the underlying mechanism a darwin registration used.
// It is empty on other platforms.
type AutostartStrategy string

const (
AutostartStrategyNone AutostartStrategy = ""
AutostartStrategySMAppService AutostartStrategy = "smappservice"
AutostartStrategyLaunchAgent AutostartStrategy = "launchagent"
AutostartStrategyRegistryRun AutostartStrategy = "registry-run"
AutostartStrategyXDGAutostart AutostartStrategy = "xdg-autostart"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 0d67b58. Rewrote the doc comment to say AutostartStrategy is used cross-platform, with darwin distinguishing between SMAppService and the LaunchAgent fallback, and Windows/Linux each having a single fixed strategy.

Comment thread v3/pkg/application/autostart.go Outdated
//
// If empty, a sensible default is derived: on macOS the application's
// bundle identifier (when running from a bundle) or "wails.autostart.<slug>";
// on Windows and Linux a slugified form of Options.Name.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 0d67b58. Reworded to make clear it's the application's application.Options.Name (from application.New), not a non-existent AutostartOptions.Name field.

Comment on lines +150 to +172
// quoteExec escapes a single Exec field token per the freedesktop.org spec:
// reserved chars are " ` $ \ → escape with backslash; if the token contains
// any reserved or whitespace, double-quote it.
func quoteExec(s string) string {
needQuote := false
var b strings.Builder
for _, r := range s {
switch r {
case '"', '`', '$', '\\':
b.WriteByte('\\')
b.WriteRune(r)
needQuote = true
case ' ', '\t', '\n':
b.WriteRune(r)
needQuote = true
default:
b.WriteRune(r)
}
}
if needQuote {
return `"` + b.String() + `"`
}
return b.String()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 0d67b58. Added validateDesktopExecToken that rejects ASCII control chars (everything below 0x20 plus 0x7F, but keeping tab and space which quoteExec handles via double-quoting). enable() now runs it against the resolved executable path and every argument before they reach quoteExec, so a newline-bearing user argument can't inject additional Desktop Entry keys. Also removed the now-dead '\n' case from quoteExec itself as a defence-in-depth measure.

/*
#cgo CFLAGS: -mmacosx-version-min=10.15 -x objective-c -Wno-unguarded-availability-new
#cgo LDFLAGS: -framework Foundation -framework ServiceManagement

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 0d67b58. Added #include <stdlib.h> (for free) and #include <string.h> (for strdup) to the C preamble.

Comment on lines +146 to +154
## Identifier

If `Options.Identifier` is empty, a default is derived from your app's name:

| Platform | Default identifier |
|---|---|
| macOS (bundled) | The app's bundle identifier, e.g. `com.example.MyApp` |
| macOS (unbundled) | `wails.autostart.<slug-of-options.Name>` |
| Windows | Slugified `Options.Name` (lowercase, non-`A-Za-z0-9._-` stripped, spaces become dashes) |

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 0d67b58. Changed every Options.Identifier reference in the docs to AutostartOptions.Identifier so readers don't go looking for it on the main app Options struct.

Comment on lines +165 to +166
- If the user renames the binary, autostart still finds the registration and cleans it up correctly.
- If the identifier was changed between releases, the old registration is still discoverable by `Status()` and can still be cleaned up by `Disable()`.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 0d67b58. You're right — the old wording oversold the guarantee. The new section says explicitly: identifier changes are safe (we find by path); symlinked-install changes are safe (filepath.EvalSymlinks is applied before matching); but arbitrary binary moves leave an orphaned entry, and apps shipped as portable binaries should either Disable() before moving themselves or launch through a stable symlink.

Comment on lines +130 to +187
// parseWindowsCommandExe returns the first token of a Windows command line,
// honouring surrounding double quotes for paths with spaces. Returned in
// lowercase for case-insensitive comparison.
func parseWindowsCommandExe(cmd string) string {
cmd = strings.TrimSpace(cmd)
if cmd == "" {
return ""
}
if cmd[0] == '"' {
end := strings.IndexByte(cmd[1:], '"')
if end < 0 {
return strings.ToLower(cmd[1:])
}
return strings.ToLower(cmd[1 : 1+end])
}
if i := strings.IndexAny(cmd, " \t"); i >= 0 {
return strings.ToLower(cmd[:i])
}
return strings.ToLower(cmd)
}

// quoteWindowsArg wraps an argument in double quotes when it contains
// whitespace or quotes, and escapes embedded quotes. Backslashes preceding a
// quote are doubled per CommandLineToArgvW rules.
func quoteWindowsArg(s string) string {
if s != "" && !strings.ContainsAny(s, ` " `) {
return s
}
var b strings.Builder
b.WriteByte('"')
backslashes := 0
for i := 0; i < len(s); i++ {
c := s[i]
switch c {
case '\\':
backslashes++
case '"':
for j := 0; j < backslashes; j++ {
b.WriteByte('\\')
}
b.WriteByte('\\')
b.WriteByte('"')
backslashes = 0
default:
for j := 0; j < backslashes; j++ {
b.WriteByte('\\')
}
backslashes = 0
b.WriteByte(c)
}
}
for j := 0; j < backslashes; j++ {
b.WriteByte('\\')
b.WriteByte('\\')
}
b.WriteByte('"')
return b.String()
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 0d67b58. Added autostart_windows_test.go covering registry round-trip, custom identifier + arguments quoting, identifier validation rejection, disable noop, parseWindowsCommandExe quoted/unquoted/leading-whitespace/empty cases, and quoteWindowsArg for the tricky CommandLineToArgvW backslash-doubling rules. Tests use the existing registrySubKey field as a test seam, writing under HKCU\\Software\\Wails\\Tests\\Autostart-<t.Name()> with t.Cleanup deleting the subkey afterwards — production HKCU\\…\\Run is never touched. Can't run them locally (macOS host) but GOOS=windows go vet is clean; they'll execute on the Wails Windows CI.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
v3/examples/autostart/main.go (1)

19-40: ⚡ Quick win

Use app.Dialog.Error() for error messages.

Lines 22, 32, and 38 display errors using app.Dialog.Info(). Error conditions should use app.Dialog.Error() to provide appropriate visual feedback (error icon, error styling).

💬 Suggested improvement
 menu.Add("Status").OnClick(func(_ *application.Context) {
   st, err := app.Autostart.Status()
   if err != nil {
-    app.Dialog.Info().SetMessage(fmt.Sprintf("Error: %v", err)).Show()
+    app.Dialog.Error().SetTitle("Status Error").SetMessage(fmt.Sprintf("%v", err)).Show()
     return
   }
   msg := fmt.Sprintf("Enabled: %v\nStrategy: %s\nPath: %s",
     st.Enabled, st.Strategy, st.Path)
   app.Dialog.Info().SetMessage(msg).Show()
 })

 menu.Add("Enable").OnClick(func(_ *application.Context) {
   if err := app.Autostart.Enable(); err != nil {
-    app.Dialog.Info().SetMessage(fmt.Sprintf("Enable failed: %v", err)).Show()
+    app.Dialog.Error().SetTitle("Enable Failed").SetMessage(fmt.Sprintf("%v", err)).Show()
   }
 })

 menu.Add("Disable").OnClick(func(_ *application.Context) {
   if err := app.Autostart.Disable(); err != nil {
-    app.Dialog.Info().SetMessage(fmt.Sprintf("Disable failed: %v", err)).Show()
+    app.Dialog.Error().SetTitle("Disable Failed").SetMessage(fmt.Sprintf("%v", err)).Show()
   }
 })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/examples/autostart/main.go` around lines 19 - 40, Replace the uses of
app.Dialog.Info() in the error branches with app.Dialog.Error() so error dialogs
use the proper styling; specifically update the Status handler error branch
(after app.Autostart.Status() returns err) and the Enable and Disable handlers
(after app.Autostart.Enable() and app.Autostart.Disable() return err) to call
app.Dialog.Error().SetMessage(...).Show() instead of
app.Dialog.Info().SetMessage(...).Show().
v3/pkg/application/autostart_windows.go (1)

30-58: ⚡ Quick win

enable() doesn't clean up an existing entry written under a different value name.

If Enable() is called once with Identifier="v1" and later with Identifier="v2" (or the slug derived from app.options.Name changes between releases), both HKCU\…\Run\v1 and HKCU\…\Run\v2 will end up pointing at the current binary. disable() walks ReadValueNames and only removes the first match, leaving a stale autostart entry behind.

Consider calling find() first and deleting any matching pre-existing value whose name differs from id before SetStringValue:

♻️ Proposed refactor
 	cmd := quoteWindowsArg(exe)
 	for _, arg := range opts.Arguments {
 		cmd += " " + quoteWindowsArg(arg)
 	}
 
 	key, _, err := registry.CreateKey(registry.CURRENT_USER, a.registrySubKey, registry.SET_VALUE)
 	if err != nil {
 		return fmt.Errorf("autostart: open registry key: %w", err)
 	}
 	defer key.Close()
 
+	// Remove any stale entry registered under a different value name.
+	if existing, _, err := a.find(); err == nil && existing != "" && existing != id {
+		_ = key.DeleteValue(existing)
+	}
 	if err := key.SetStringValue(id, cmd); err != nil {
 		return fmt.Errorf("autostart: write registry value: %w", err)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/pkg/application/autostart_windows.go` around lines 30 - 58, The enable()
routine can leave stale registry values when the autostart identifier changes;
before writing the new value with key.SetStringValue(id, cmd) call find() (or
use registry.Key.ReadValueNames) to enumerate existing Run entries, delete any
matching autostart entry whose name != id (using key.DeleteValue(name)) so old
names (e.g., previous Identifier values or old autostartSlug names) are removed,
then proceed to SetStringValue; mirror this logic with the existing
find()/disable() helpers to ensure consistent identification and cleanup.
v3/pkg/application/autostart_darwin.go (1)

244-259: 💤 Low value

Dead plist types — plistDoc, plistDict, plistEl are unused.

The XML is produced manually via strings.Builder in launchAgentPlist (lines 261‑283), so these struct declarations are not referenced anywhere in the file or the test file. Consider removing them, or wire launchAgentPlist to use xml.Marshal against them — but not both.

🧹 Proposed cleanup
-// --- plist marshalling ------------------------------------------------------
-
-type plistDoc struct {
-	XMLName xml.Name  `xml:"plist"`
-	Version string    `xml:"version,attr"`
-	Dict    plistDict `xml:"dict"`
-}
-
-type plistDict struct {
-	Keys   []string  `xml:"-"`
-	Values []plistEl `xml:"-"`
-}
-
-type plistEl struct {
-	Kind   string   // "string", "true", "false", "array"
-	String string
-	Array  []string // for "array" of strings
-}
-
+// --- plist marshalling ------------------------------------------------------
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/pkg/application/autostart_darwin.go` around lines 244 - 259, The types
plistDoc, plistDict, and plistEl are dead code because launchAgentPlist builds
the XML manually; either remove these unused struct declarations to clean up the
file or modify launchAgentPlist to construct a plistDoc (with plistDict/plistEl
children) and serialize it with xml.Marshal instead of manual string building;
locate the symbols plistDoc, plistDict, plistEl and the launchAgentPlist
function and choose one approach (delete the structs or wire launchAgentPlist to
use xml.Marshal) to eliminate the inconsistency.
v3/pkg/application/autostart_linux.go (1)

20-49: ⚡ Quick win

enable() leaves stale .desktop files behind when the identifier changes.

Mirrors the Windows issue: if Enable() is called once with no identifier (uses the slug of app.options.Name), and a later release renames the app or supplies a custom Identifier, both .desktop files end up pointing at the same exe. findDesktopFile() then returns whichever is first in os.ReadDir, so disable() only removes one and status() reports Enabled=true indefinitely (with the user-visible Path arbitrarily one or the other).

Consider sweeping out any pre-existing matching entry whose filename differs from the new id before writing:

♻️ Proposed refactor
 	id := opts.Identifier
 	if id == "" {
 		id = autostartSlug(a.app.options.Name)
 	}
 	path := filepath.Join(dir, id+".desktop")
 
+	// Remove any stale autostart entry registered under a different identifier
+	// that points at the same executable.
+	if existing, err := a.findDesktopFile(dir); err == nil && existing != "" && existing != path {
+		_ = os.Remove(existing)
+	}
+
 	body := buildDesktopEntry(a.app.options.Name, exe, opts.Arguments)
 	if err := writeFileAtomic(path, []byte(body), 0644); err != nil {
 		return fmt.Errorf("write desktop file %s: %w", path, err)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/pkg/application/autostart_linux.go` around lines 20 - 49, The enable()
implementation can leave stale .desktop files when the identifier changes;
before writing the new desktop file, scan the autostart directory (use
autostartDir()/os.ReadDir) for existing .desktop files whose Exec (or full
desktop entry contents) points to the same resolved executable
(resolvedExecutable()) and, if their filename != new id+".desktop", remove them
so only the intended id remains; do this sweep in enable() prior to
writeFileAtomic(path, ...) so findDesktopFile()/disable() and status() see a
single consistent entry (mirror the fix used for Windows identifiers and ensure
you reference autostartSlug(a.app.options.Name) when computing the fallback id).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@v3/pkg/application/autostart_darwin.go`:
- Around line 75-104: The status() method currently treats
errSMAppServiceRequiresApproval from smAppServiceIsEnabled() as a fatal error;
update status() so that it treats errSMAppServiceRequiresApproval the same as
errSMAppServiceUnavailable (i.e., do not return an error, just treat the
SMAppService as "not enabled" and continue to the LaunchAgent fallback),
ensuring smAppServiceIsEnabled() errors are only propagated for unexpected
errors; adjust the guard around the smAppServiceIsEnabled() call to allow both
errSMAppServiceUnavailable and errSMAppServiceRequiresApproval to be ignored and
still call findLaunchAgent() and return the appropriate AutostartStatus.

In `@v3/pkg/application/autostart_windows.go`:
- Around line 154-187: The quoteWindowsArg function mis-escapes backslashes
before a quote: when handling case '"' it currently writes only backslashes+1
slashes, but per CommandLineToArgvW you must emit 2*backslashes+1 backslashes
before a literal quote; update the '"' branch in quoteWindowsArg to write
2*backslashes backslashes plus the extra one (so total 2*backslashes+1) before
writing the quote, keep the existing handling for other characters and the
trailing-backslash loop, and add Windows unit tests for inputs like `a\"b`,
`a\\"b`, and a trailing `\\` to prevent regressions.

---

Nitpick comments:
In `@v3/examples/autostart/main.go`:
- Around line 19-40: Replace the uses of app.Dialog.Info() in the error branches
with app.Dialog.Error() so error dialogs use the proper styling; specifically
update the Status handler error branch (after app.Autostart.Status() returns
err) and the Enable and Disable handlers (after app.Autostart.Enable() and
app.Autostart.Disable() return err) to call
app.Dialog.Error().SetMessage(...).Show() instead of
app.Dialog.Info().SetMessage(...).Show().

In `@v3/pkg/application/autostart_darwin.go`:
- Around line 244-259: The types plistDoc, plistDict, and plistEl are dead code
because launchAgentPlist builds the XML manually; either remove these unused
struct declarations to clean up the file or modify launchAgentPlist to construct
a plistDoc (with plistDict/plistEl children) and serialize it with xml.Marshal
instead of manual string building; locate the symbols plistDoc, plistDict,
plistEl and the launchAgentPlist function and choose one approach (delete the
structs or wire launchAgentPlist to use xml.Marshal) to eliminate the
inconsistency.

In `@v3/pkg/application/autostart_linux.go`:
- Around line 20-49: The enable() implementation can leave stale .desktop files
when the identifier changes; before writing the new desktop file, scan the
autostart directory (use autostartDir()/os.ReadDir) for existing .desktop files
whose Exec (or full desktop entry contents) points to the same resolved
executable (resolvedExecutable()) and, if their filename != new id+".desktop",
remove them so only the intended id remains; do this sweep in enable() prior to
writeFileAtomic(path, ...) so findDesktopFile()/disable() and status() see a
single consistent entry (mirror the fix used for Windows identifiers and ensure
you reference autostartSlug(a.app.options.Name) when computing the fallback id).

In `@v3/pkg/application/autostart_windows.go`:
- Around line 30-58: The enable() routine can leave stale registry values when
the autostart identifier changes; before writing the new value with
key.SetStringValue(id, cmd) call find() (or use registry.Key.ReadValueNames) to
enumerate existing Run entries, delete any matching autostart entry whose name
!= id (using key.DeleteValue(name)) so old names (e.g., previous Identifier
values or old autostartSlug names) are removed, then proceed to SetStringValue;
mirror this logic with the existing find()/disable() helpers to ensure
consistent identification and cleanup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 51ba7306-d7fe-4bcc-b6a6-7e7af20d2c19

📥 Commits

Reviewing files that changed from the base of the PR and between 278bb28 and 7aa112b.

📒 Files selected for processing (17)
  • docs/astro.config.mjs
  • docs/src/content/docs/concepts/manager-api.mdx
  • docs/src/content/docs/features/autostart/basics.mdx
  • v3/examples/autostart/main.go
  • v3/pkg/application/application.go
  • v3/pkg/application/autostart.go
  • v3/pkg/application/autostart_android.go
  • v3/pkg/application/autostart_darwin.go
  • v3/pkg/application/autostart_darwin_smappservice.go
  • v3/pkg/application/autostart_darwin_test.go
  • v3/pkg/application/autostart_ios.go
  • v3/pkg/application/autostart_linux.go
  • v3/pkg/application/autostart_linux_test.go
  • v3/pkg/application/autostart_manager.go
  • v3/pkg/application/autostart_server.go
  • v3/pkg/application/autostart_test.go
  • v3/pkg/application/autostart_windows.go

Comment thread v3/pkg/application/autostart_darwin.go
Comment thread v3/pkg/application/autostart_windows.go
- autostart.go: defensive short-write check in writeFileAtomic (n
  vs len(data)); doc comment for AutostartStrategy clarifies it is
  used cross-platform, not darwin-only; AutostartOptions.Identifier
  doc clarifies the Name source is application.Options.Name.
- autostart_linux.go: validateDesktopExecToken rejects ASCII control
  characters in the executable path and arguments before they reach
  quoteExec, preventing a newline-bearing argument from injecting
  additional Desktop Entry keys into the .desktop file.
- autostart_darwin_smappservice.go: explicit <stdlib.h> and
  <string.h> includes for strdup/free so the C preamble compiles
  even when toolchain flags would otherwise warn on implicit
  declarations.
- autostart_windows_test.go: registry round-trip, custom identifier,
  identifier validation, disable noop, parseWindowsCommandExe, and
  quoteWindowsArg cases — using the existing test seam to read/write
  under HKCU\\Software\\Wails\\Tests\\Autostart-<t.Name()> so prod
  HKCU\\…\\Run is never touched.
- docs/features/autostart/basics.mdx: rename Options.Identifier to
  AutostartOptions.Identifier; rewrite the stale-detection section
  so it accurately describes path-based matching — identifier and
  symlinked-install changes are handled; arbitrary binary moves
  leave an orphaned entry.
@leaanthony

Copy link
Copy Markdown
Member Author

Addressed all 8 of @copilot-pull-request-reviewer's inline comments in `0d67b58`:

File Issue Fix
`autostart.go:40` `writeFileAtomic` could silently truncate on a short write Defensive `n == len(data)` check returning `io.ErrShortWrite`
`autostart.go:118` Identifier doc said "Options.Name" ambiguously Clarified as `application.Options.Name` (from `application.New`)
`autostart.go:134` `AutostartStrategy` comment claimed darwin-only Reworded to describe cross-platform use, with macOS distinguishing between SMAppService and LaunchAgent
`autostart_linux.go:172` `quoteExec` accepted newlines → Desktop Entry key injection risk New `validateDesktopExecToken` rejects ASCII control chars before `quoteExec` sees them; `enable()` validates the exe path and every argument
`autostart_darwin_smappservice.go:8` `strdup` / `free` used without their headers Added `<stdlib.h>` and `<string.h>` to the C preamble
`docs/.../basics.mdx:154` "Options.Identifier" — wrong type Renamed to `AutostartOptions.Identifier`
`docs/.../basics.mdx:166` Claimed renaming the binary still finds the registration (it doesn't) Rewrote: identifier changes and symlinked installs are safe; arbitrary binary moves orphan the entry — apps shipping as portable binaries should `Disable()` first or use a stable symlink
`autostart_windows.go:187` No Windows-tagged tests New `autostart_windows_test.go`: registry round-trip, custom identifier + argument quoting, identifier validation, disable noop, `parseWindowsCommandExe`, `quoteWindowsArg`. Uses the existing `registrySubKey` test seam pointing at a per-`t.Name()` subkey under `HKCU\\Software\\Wails\\Tests\\` cleaned up via `t.Cleanup`

Verified: darwin build + tests pass; linux build + tests pass on `lin-node1`; `GOOS=windows go vet` clean. Each Copilot comment also has an inline reply pointing to the specific change.

Two actionable findings + four nitpicks:

1. (Major) quoteWindowsArg mis-escaped a backslash immediately preceding
   a literal quote: emitted (N+1) backslashes when CommandLineToArgvW
   requires (2N+1). For input `a\"b` the old output `"a\\"b"` would
   decode as `a\` (the quote toggled state and was lost) instead of
   `a\"b`. The existing Windows test cases already encoded the correct
   2N+1 expectations, so they begin passing with this fix.

2. (Minor) darwin status() treated errSMAppServiceRequiresApproval (the
   user disabled the login item in System Settings) as a fatal error,
   skipping the LaunchAgent fallback. Now tolerated alongside
   errSMAppServiceUnavailable so the fallback can still find a legacy
   LaunchAgent.

3. (Nit) autostart_windows.go and autostart_linux.go enable() now
   sweep any pre-existing autostart entry that points at this binary
   under a different identifier before writing the new one. Without
   this, calling Enable() twice with different Identifiers (or with a
   slug that changed because Options.Name was renamed between releases)
   left a stale duplicate behind.

4. (Nit) Removed dead plistDoc / plistDict / plistEl types from
   autostart_darwin.go — leftover from an iteration that used
   xml.Marshal before switching to manual string building.

5. (Nit) examples/autostart/main.go uses Dialog.Error() for failure
   branches instead of Dialog.Info(), giving error dialogs the
   expected icon + styling.
@leaanthony

Copy link
Copy Markdown
Member Author

Addressed all six of @coderabbitai's findings (2 actionable + 4 nitpicks) in `51eebc9`:

File Severity Issue Fix
`autostart_windows.go:154-187` 🟠 Major `quoteWindowsArg` mis-escaped backslash-before-quote (`N+1` instead of `2N+1` per `CommandLineToArgvW`) `case '"'` now writes `2*backslashes` + 1 slashes. The Windows tests added in the previous commit already encoded the correct expectations, so they start passing rather than needing updates.
`autostart_darwin.go:75-104` 🟡 Minor `status()` surfaced `errSMAppServiceRequiresApproval` as a fatal error and skipped the LaunchAgent fallback Tolerated alongside `errSMAppServiceUnavailable` so the LaunchAgent path still runs for users who disabled the login item in System Settings.
`examples/autostart/main.go:19-40` 🧹 Nit Error branches used `Dialog.Info()` Switched to `Dialog.Error().SetTitle(…).SetMessage(err.Error()).Show()` for the three error sites.
`autostart_windows.go:30-58` 🧹 Nit `enable()` could leave a duplicate registry entry under the old value name when `Identifier` changed between releases `enable()` now calls `find()` first and deletes any pre-existing entry whose value name differs from the new id before `SetStringValue`.
`autostart_linux.go:20-49` 🧹 Nit Same shape on Linux — `enable()` could leave a stale `.desktop` file when the identifier changed Sweeps any pre-existing entry pointing at this binary whose path differs from the new one before `writeFileAtomic`.
`autostart_darwin.go:244-259` 🧹 Nit Dead `plistDoc` / `plistDict` / `plistEl` types Removed (the plist is built manually via `strings.Builder` in `launchAgentPlist` and these were leftover from an earlier `xml.Marshal` iteration).

Verified: darwin build + tests pass; linux build + tests pass on lin-node1; `GOOS=windows go vet` clean. Each actionable comment also has an inline reply pointing at the specific commit.

@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown
Contributor

@leaanthony Thanks for the thorough follow-up! All six findings look well-addressed:

  • quoteWindowsArg — The 2N+1 backslash fix is the most impactful one; glad the pre-existing Windows tests already encoded the correct expectations so they validate the fix automatically.
  • status() on Darwin — Tolerating errSMAppServiceRequiresApproval alongside errSMAppServiceUnavailable is the right call for users who revoked the login-item permission in System Settings.
  • Stale-entry sweeps — Both the Windows registry and Linux .desktop sweeps are solid defensive improvements for apps that change their identifier between releases.
  • Dead plist types — Good cleanup removing those xml.Marshal leftovers.

I'll trigger a fresh review to check the full state of 51eebc9.

🐇 ✨

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@leaanthony
leaanthony merged commit 3ca0fb6 into master May 13, 2026
119 checks passed
@leaanthony
leaanthony deleted the feat/autostart branch May 13, 2026 20:18
@leaanthony leaanthony mentioned this pull request Aug 26, 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