Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion app/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,13 @@ type State struct {
AllBadge widget.Clickable // "All" filter badge

// Settings view state (UI-THREAD-ONLY)
SettingsList widget.List
SettingsList widget.List
AutoStartEnabled bool // Whether autostart is currently on
AutoStartClick widget.Clickable // Toggle button
AutoStartError string // Error message after toggle attempt
AutoStartSuccess bool // Show success message after toggle
AutoStartNeedsUpdate bool // Deferred toggle flag (like Hotkeys.NeedsUpdate)
AutoStartUpdating bool // Guard: set on UI thread before goroutine launch, cleared by goroutine before Invalidate

// Background-to-UI invalidation flag.
// Background goroutines set this (via MarkDirty) alongside Window.Invalidate().
Expand Down
5 changes: 0 additions & 5 deletions data/syntax/braces.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,6 @@ func (s *ScannerState) Advance(ch byte) bool {
return !s.inSingleQuote && !s.inDoubleQuote
}

// InQuote reports whether the scanner is currently inside a quoted string.
func (s *ScannerState) InQuote() bool {
return s.inSingleQuote || s.inDoubleQuote
}

// IsBalancedBraces checks if braces are balanced in a command,
// respecting quotes.
func IsBalancedBraces(command string) bool {
Expand Down
20 changes: 0 additions & 20 deletions data/syntax/braces_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,26 +55,6 @@ func TestScannerStateAdvance(t *testing.T) {
}
}

func TestScannerStateInQuote(t *testing.T) {
var s ScannerState

if s.InQuote() {
t.Error("expected InQuote() = false for initial state")
}

s.Advance('"')

if !s.InQuote() {
t.Error("expected InQuote() = true after opening double quote")
}

s.Advance('"')

if s.InQuote() {
t.Error("expected InQuote() = false after closing double quote")
}
}

func TestBalancedBraces(t *testing.T) {
tests := []struct {
input string
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/godbus/dbus/v5 v5.2.2
golang.design/x/hotkey v0.4.1
golang.org/x/exp/shiny v0.0.0-20260212183809-81e46e3db34a
golang.org/x/sys v0.41.0
)

require (
Expand All @@ -32,6 +33,5 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.1 // indirect
golang.org/x/image v0.36.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
)
110 changes: 110 additions & 0 deletions infra/autostart/autostart_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//go:build darwin

package autostart

import (
"encoding/xml"
"fmt"
"os"
"path/filepath"
"strings"
)

const (
launchAgentLabel = "com.debrief"
plistName = launchAgentLabel + ".plist"
plistDir = "Library/LaunchAgents"
launchAgentsDirPerm = 0o750
plistFilePerm = 0o600
)

// Enable registers the app as a macOS LaunchAgent to start on login.
func Enable() error {
exePath, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to get executable path: %w", err)
}

plistPath, err := launchAgentPath()
if err != nil {
return err
}

dir := filepath.Dir(plistPath)
if err := os.MkdirAll(dir, launchAgentsDirPerm); err != nil {
return fmt.Errorf("failed to create LaunchAgents directory: %w", err)
}

var escaped strings.Builder
if err := xml.EscapeText(&escaped, []byte(exePath)); err != nil {
return fmt.Errorf("failed to escape executable path: %w", err)
}

content := `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>` + launchAgentLabel + `</string>
<key>ProgramArguments</key>
<array>
<string>` + escaped.String() + `</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
`

if err := os.WriteFile(plistPath, []byte(content), plistFilePerm); err != nil {
return fmt.Errorf("failed to write LaunchAgent plist: %w", err)
}

return nil
}

// Disable removes the LaunchAgent plist to stop the app from starting on login.
func Disable() error {
plistPath, err := launchAgentPath()
if err != nil {
return err
}

if err := os.Remove(plistPath); err != nil {
if os.IsNotExist(err) {
return nil
}

return fmt.Errorf("failed to remove LaunchAgent plist: %w", err)
}

return nil
}

// IsEnabled checks whether the LaunchAgent plist exists.
func IsEnabled() (bool, error) {
plistPath, err := launchAgentPath()
if err != nil {
return false, err
}

_, err = os.Stat(plistPath)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}

return false, fmt.Errorf("failed to check LaunchAgent plist: %w", err)
}

return true, nil
}

func launchAgentPath() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("failed to get home directory: %w", err)
}

return filepath.Join(home, plistDir, plistName), nil
}
99 changes: 99 additions & 0 deletions infra/autostart/autostart_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//go:build linux

package autostart

import (
"fmt"
"os"
"path/filepath"
)

const (
desktopFileName = "debrief.desktop"
autostartDir = "autostart"
autostartDirPerm = 0o750
desktopFilePerm = 0o600
)

// Enable creates an XDG autostart .desktop file so the app starts on login.
func Enable() error {
exePath, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to get executable path: %w", err)
}

desktopPath, err := autostartFilePath()
if err != nil {
return err
}

dir := filepath.Dir(desktopPath)
if err := os.MkdirAll(dir, autostartDirPerm); err != nil {
return fmt.Errorf("failed to create autostart directory: %w", err)
}

content := "[Desktop Entry]\n" +
"Type=Application\n" +
"Name=Debrief\n" +
"Exec=" + exePath + "\n" +
"X-GNOME-Autostart-enabled=true\n" +
"StartupNotify=false\n" +
"Terminal=false\n"

if err := os.WriteFile(desktopPath, []byte(content), desktopFilePerm); err != nil {
return fmt.Errorf("failed to write autostart desktop file: %w", err)
}

return nil
}

// Disable removes the XDG autostart .desktop file.
func Disable() error {
desktopPath, err := autostartFilePath()
if err != nil {
return err
}

if err := os.Remove(desktopPath); err != nil {
if os.IsNotExist(err) {
return nil
}

return fmt.Errorf("failed to remove autostart desktop file: %w", err)
}

return nil
}

// IsEnabled checks whether the XDG autostart .desktop file exists.
func IsEnabled() (bool, error) {
desktopPath, err := autostartFilePath()
if err != nil {
return false, err
}

_, err = os.Stat(desktopPath)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}

return false, fmt.Errorf("failed to check autostart desktop file: %w", err)
}

return true, nil
}

func autostartFilePath() (string, error) {
configDir := os.Getenv("XDG_CONFIG_HOME")
if configDir == "" {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("failed to get home directory: %w", err)
}

configDir = filepath.Join(home, ".config")
}

return filepath.Join(configDir, autostartDir, desktopFileName), nil
}
75 changes: 75 additions & 0 deletions infra/autostart/autostart_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//go:build windows

package autostart

import (
"errors"
"fmt"
"os"

"golang.org/x/sys/windows/registry"
)

const (
registryPath = `SOFTWARE\Microsoft\Windows\CurrentVersion\Run`
valueName = "Debrief"
)

// Enable registers the app to start on login via the Windows registry.
func Enable() error {
exePath, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to get executable path: %w", err)
}

key, _, err := registry.CreateKey(registry.CURRENT_USER, registryPath, registry.SET_VALUE)
if err != nil {
return fmt.Errorf("failed to open registry key: %w", err)
}
defer key.Close() //nolint:errcheck // registry key close errors are non-actionable

if err := key.SetStringValue(valueName, exePath); err != nil {
return fmt.Errorf("failed to set registry value: %w", err)
}

return nil
}

// Disable removes the app from login startup in the Windows registry.
func Disable() error {
key, err := registry.OpenKey(registry.CURRENT_USER, registryPath, registry.SET_VALUE)
if err != nil {
return fmt.Errorf("failed to open registry key: %w", err)
}
defer key.Close() //nolint:errcheck // registry key close errors are non-actionable

if err := key.DeleteValue(valueName); err != nil {
if errors.Is(err, registry.ErrNotExist) {
return nil
}

return fmt.Errorf("failed to delete registry value: %w", err)
}

return nil
}

// IsEnabled checks whether the app is registered for login startup.
func IsEnabled() (bool, error) {
key, err := registry.OpenKey(registry.CURRENT_USER, registryPath, registry.QUERY_VALUE)
if err != nil {
return false, nil
}
defer key.Close() //nolint:errcheck // registry key close errors are non-actionable

_, _, err = key.GetStringValue(valueName)
if err != nil {
if errors.Is(err, registry.ErrNotExist) {
return false, nil
}

return false, fmt.Errorf("failed to read registry value: %w", err)
}

return true, nil
}
3 changes: 3 additions & 0 deletions infra/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ type Config struct {

HotkeyPreset int `json:"hotkeyPreset"` // Preset index (0, 1, or 2)

AutoStart bool `json:"autoStart,omitempty"` // Start on computer boot

// Window geometry persisted across restarts (pixels).
// Zero values mean "use default".
WindowW int `json:"windowW,omitempty"`
Expand All @@ -27,6 +29,7 @@ func DefaultConfig() *Config {
return &Config{
Version: SettingsVersion,
HotkeyPreset: 0,
AutoStart: true,
}
}

Expand Down
Loading
Loading