diff --git a/app.go b/app.go index bd14b61..8b8757b 100644 --- a/app.go +++ b/app.go @@ -476,7 +476,7 @@ func (a *App) startupHeadless() { } // Ensure desktop integration for Wayland portals - if err := platform.EnsureDesktopFile(); err != nil { + if err := platform.EnsureDesktopFile(tray.DefaultIconBytes()); err != nil { logger.Error("Failed to ensure desktop file: %v", err) } diff --git a/internal/linux/startup.go b/internal/linux/startup.go index ea017e3..1e82904 100644 --- a/internal/linux/startup.go +++ b/internal/linux/startup.go @@ -15,31 +15,52 @@ const baseDesktopFileTemplate = `[Desktop Entry] Type=Application Name=WIS Free V3 %s +Icon=%s +StartupWMClass=%s Terminal=false Categories=Utility; ` -const autostartDesktopFileTemplate = baseDesktopFileTemplate + "X-GNOME-Autostart-enabled=true\n" +const autostartDesktopFileTemplate = `[Desktop Entry] +Type=Application +Name=WIS Free V3 +%s +Icon=wis-free-v3 +StartupWMClass=wis-free-v3 +Terminal=false +Categories=Utility; +X-GNOME-Autostart-enabled=true +` -func EnsureDesktopFile() error { +func EnsureDesktopFile(iconBytes []byte) 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)) + + // Write icon to local pixmaps so the desktop file Icon= field resolves + pixmapsDir := filepath.Join(home, ".local", "share", "pixmaps") + if err := os.MkdirAll(pixmapsDir, 0755); err != nil { + return err + } + iconPath := filepath.Join(pixmapsDir, appName+".png") + if err := os.WriteFile(iconPath, iconBytes, 0644); err != nil { + return fmt.Errorf("failed to write icon to pixmaps: %w", err) + } + + content := fmt.Sprintf(baseDesktopFileTemplate, desktopExecField(exePath), appName, appName) return os.WriteFile(desktopPath, []byte(content), 0644) } diff --git a/internal/platform/platform_linux.go b/internal/platform/platform_linux.go index bc86e72..1ca1003 100644 --- a/internal/platform/platform_linux.go +++ b/internal/platform/platform_linux.go @@ -31,6 +31,6 @@ func IsProcessRunning(pid int) bool { return linux.IsProcessRunning(pid) } -func EnsureDesktopFile() error { - return linux.EnsureDesktopFile() +func EnsureDesktopFile(iconBytes []byte) error { + return linux.EnsureDesktopFile(iconBytes) } diff --git a/internal/platform/platform_windows.go b/internal/platform/platform_windows.go index 72ec01d..9ddbb81 100644 --- a/internal/platform/platform_windows.go +++ b/internal/platform/platform_windows.go @@ -31,7 +31,7 @@ func IsProcessRunning(pid int) bool { return windows.IsProcessRunning(pid) } -func EnsureDesktopFile() error { +func EnsureDesktopFile(iconBytes []byte) error { // Not applicable on Windows return nil } diff --git a/internal/ui/tray/icon.png b/internal/ui/tray/icon.png new file mode 100644 index 0000000..63617fe Binary files /dev/null and b/internal/ui/tray/icon.png differ diff --git a/internal/ui/tray/tray.go b/internal/ui/tray/tray.go index 90b98de..dc3c865 100644 --- a/internal/ui/tray/tray.go +++ b/internal/ui/tray/tray.go @@ -10,7 +10,6 @@ import ( "os" "runtime" "strings" - "sync" "wis-free-v3/internal/config" "wis-free-v3/internal/logger" @@ -22,6 +21,9 @@ import ( //go:embed icon.ico var iconData []byte +//go:embed icon.png +var iconPNGData []byte + //go:embed icon_recording.png var iconRecordingData []byte @@ -152,30 +154,16 @@ func buildTooltip(app App) string { return trayLabel + " - " + shortcut + " to record" } -var ( - defaultIconOnce sync.Once - defaultIconBytes []byte -) - func getDefaultIcon() []byte { - if runtime.GOOS != "linux" { - return iconData + if runtime.GOOS == "linux" { + return iconPNGData } + return iconData +} - defaultIconOnce.Do(func() { - pngData, err := icoToPNG(iconData) - if err != nil { - logger.Error("Failed to convert tray icon to PNG for Linux: %v", err) - defaultIconBytes = iconData - return - } - defaultIconBytes = pngData - }) - - if len(defaultIconBytes) == 0 { - return iconData - } - return defaultIconBytes +// DefaultIconBytes returns the raw default icon bytes for the current platform. +func DefaultIconBytes() []byte { + return getDefaultIcon() } func icoToPNG(data []byte) ([]byte, error) { diff --git a/internal/ui/tray/tray_linux.go b/internal/ui/tray/tray_linux.go new file mode 100644 index 0000000..a6f268e --- /dev/null +++ b/internal/ui/tray/tray_linux.go @@ -0,0 +1,14 @@ +//go:build linux +package tray + +/* +#include +*/ +import "C" +import "unsafe" + +func init() { + cstr := C.CString("wis-free-v3") + defer C.free(unsafe.Pointer(cstr)) + C.g_set_prgname(cstr) +} diff --git a/scripts/build-linux.sh b/scripts/build-linux.sh index d78dde7..da37ae1 100755 --- a/scripts/build-linux.sh +++ b/scripts/build-linux.sh @@ -56,6 +56,43 @@ fi # 2. Check for Linux dependencies echo "[1/3] Checking system dependencies..." MISSING_DEPS=0 + +# 2a. GNOME tray icon support check (before build — avoids launching app if this will fail) +# AppIndicator icons need the GNOME Shell extension on GNOME desktop environments. +# Other DEs (KDE Plasma, XFCE, Cinnamon, etc.) usually support them natively. +if [ "$XDG_CURRENT_DESKTOP" = "GNOME" ] || [ "$XDG_CURRENT_DESKTOP" = "ubuntu:GNOME" ]; then + if ! gnome-extensions show appindicator@altamirano.fabio &>/dev/null; then + echo "" + echo "===============================================================" + echo " ERROR: GNOME AppIndicator extension is not installed" + echo "===============================================================" + echo "" + echo " GNOME does not show system tray icons without this extension." + echo " The app will run but the tray icon will be invisible." + echo "" + if command_exists dnf; then + echo " Install it with:" + echo "" + echo " sudo dnf install gnome-shell-extension-appindicator" + echo "" + elif command_exists apt-get; then + echo " Install it with:" + echo "" + echo " sudo apt install gnome-shell-extension-appindicator" + echo "" + else + echo " Install the 'gnome-shell-extension-appindicator' package" + echo " for your distribution." + echo "" + fi + echo " Then log out and back in, and enable it in:" + echo " Settings > Extensions > AppIndicator and KStatusNotifierItem Support" + echo "" + echo "===============================================================" + exit 1 + fi +fi + DEPS=("gcc" "pkg-config") for dep in "${DEPS[@]}"; do @@ -200,6 +237,10 @@ if [ ! -f "$EXECUTABLE" ]; then exit 1 fi +# wails build sometimes auto-launches the binary on Linux — kill it so it +# doesn't block the terminal while we ask about installation. +pkill -f "$EXECUTABLE" 2>/dev/null || true + echo "[3/3] Build successful!" echo " Output: $EXECUTABLE" echo "" @@ -232,6 +273,7 @@ Comment=Voice Dictation App Exec=/usr/local/bin/$APP_NAME TryExec=/usr/local/bin/$APP_NAME Icon=$APP_NAME +StartupWMClass=$APP_NAME Terminal=false Categories=Utility;Audio; EOF