diff --git a/app.go b/app.go index 24ef537..bd14b61 100644 --- a/app.go +++ b/app.go @@ -112,6 +112,8 @@ func (a *App) StartRecording() { a.overlay.Show("Recording...") } + tray.IncrementTriggerCount() + // 2. Start recorder as soon as possible if a.audioRecorder == nil { logger.Error("Recorder not initialized") @@ -473,6 +475,11 @@ func (a *App) startupHeadless() { } } + // Ensure desktop integration for Wayland portals + if err := platform.EnsureDesktopFile(); err != nil { + logger.Error("Failed to ensure desktop file: %v", err) + } + // Initialize Hotkey Listener a.hotkeyListener = hotkey.NewListener(a.config.Shortcut, a.StartRecording, a.StopRecording) a.hotkeyListener.Start() diff --git a/internal/hotkey/hotkey.go b/internal/hotkey/hotkey.go index 828e9fe..ebc0faf 100644 --- a/internal/hotkey/hotkey.go +++ b/internal/hotkey/hotkey.go @@ -81,18 +81,35 @@ func (l *Listener) Start() { } l.hk = xhk.New(mods, key) - if err := l.hk.Register(); err != nil { - logger.Error("Failed to register hotkey %s: %v", l.shortcut, err) - l.hk = nil - l.mu.Unlock() - return - } - l.isListening = true + hkToRegister := l.hk + shortcutToRegister := l.shortcut l.mu.Unlock() - logger.Info("Hotkey listener started, waiting for %s", l.shortcut) - go l.eventLoop(l.hk) + go func() { + if err := hkToRegister.Register(); err != nil { + logger.Error("Failed to register hotkey %s: %v", shortcutToRegister, err) + l.mu.Lock() + if l.hk == hkToRegister { + l.hk = nil + l.isListening = false + } + l.mu.Unlock() + return + } + + l.mu.Lock() + if l.hk != hkToRegister { + // Was stopped or updated while we were registering + l.mu.Unlock() + hkToRegister.Unregister() + return + } + l.mu.Unlock() + + logger.Info("Hotkey listener started, waiting for %s", shortcutToRegister) + go l.eventLoop(hkToRegister) + }() } // Stop terminates the hotkey listener. diff --git a/internal/linux/startup.go b/internal/linux/startup.go index a8e7cf5..ea017e3 100644 --- a/internal/linux/startup.go +++ b/internal/linux/startup.go @@ -11,15 +11,38 @@ const appName = "wis-free-v3" // desktopFileTemplate is filled with execLine built from the absolute binary path. // Paths with spaces must be quoted per the Desktop Entry spec. -const desktopFileTemplate = `[Desktop Entry] +const baseDesktopFileTemplate = `[Desktop Entry] Type=Application Name=WIS Free V3 %s Terminal=false Categories=Utility; -X-GNOME-Autostart-enabled=true ` +const autostartDesktopFileTemplate = baseDesktopFileTemplate + "X-GNOME-Autostart-enabled=true\n" + +func EnsureDesktopFile() error { + home, err := os.UserHomeDir() + if err != nil { + return err + } + + appsDir := filepath.Join(home, ".local", "share", "applications") + if err := os.MkdirAll(appsDir, 0755); err != nil { + return err + } + + desktopPath := filepath.Join(appsDir, appName+".desktop") + + exePath, err := getExecutablePath() + if err != nil { + return err + } + + content := fmt.Sprintf(baseDesktopFileTemplate, desktopExecField(exePath)) + return os.WriteFile(desktopPath, []byte(content), 0644) +} + func getAutostartPath() (string, error) { home, err := os.UserHomeDir() if err != nil { @@ -80,7 +103,7 @@ func AddToStartup() error { return fmt.Errorf("failed to get executable path: %w", err) } - content := fmt.Sprintf(desktopFileTemplate, desktopExecField(exePath)) + content := fmt.Sprintf(autostartDesktopFileTemplate, desktopExecField(exePath)) if err := os.WriteFile(autostartPath, []byte(content), 0644); err != nil { return fmt.Errorf("failed to write autostart file: %w", err) } diff --git a/internal/platform/platform_linux.go b/internal/platform/platform_linux.go index dbd9662..bc86e72 100644 --- a/internal/platform/platform_linux.go +++ b/internal/platform/platform_linux.go @@ -30,3 +30,7 @@ func IsInStartup() bool { func IsProcessRunning(pid int) bool { return linux.IsProcessRunning(pid) } + +func EnsureDesktopFile() error { + return linux.EnsureDesktopFile() +} diff --git a/internal/platform/platform_windows.go b/internal/platform/platform_windows.go index 326f47e..72ec01d 100644 --- a/internal/platform/platform_windows.go +++ b/internal/platform/platform_windows.go @@ -30,3 +30,8 @@ func IsInStartup() bool { func IsProcessRunning(pid int) bool { return windows.IsProcessRunning(pid) } + +func EnsureDesktopFile() error { + // Not applicable on Windows + return nil +} diff --git a/internal/ui/tray/tray.go b/internal/ui/tray/tray.go index f96f1c3..43bc024 100644 --- a/internal/ui/tray/tray.go +++ b/internal/ui/tray/tray.go @@ -37,6 +37,8 @@ var trayLabel = "wis-free-v3" // Menu item references for dynamic updates var statusMenuItem *systray.MenuItem +var triggerCountItem *systray.MenuItem +var triggerCount int // Start initializes and runs the system tray. // This function blocks until the tray is terminated. @@ -74,6 +76,9 @@ func onReady(app App) { statusMenuItem = systray.AddMenuItem("Status: Ready", "Current application status") statusMenuItem.Disable() + triggerCountItem = systray.AddMenuItem("Shortcut detected: 0 times", "Troubleshooting counter") + triggerCountItem.Disable() + systray.AddSeparator() menuSettings := systray.AddMenuItem("Settings", "Open settings window") @@ -159,6 +164,14 @@ func UpdateStatus(status string) { } } +// IncrementTriggerCount increments the troubleshooting counter in the tray. +func IncrementTriggerCount() { + triggerCount++ + if triggerCountItem != nil { + triggerCountItem.SetTitle(fmt.Sprintf("Shortcut detected: %d times", triggerCount)) + } +} + // onExit is called when the system tray is shutting down. func onExit() { logger.Info("System tray terminated")