fix: Add cross-platform handling for tray, updater, and icon generation

- Add platform notes for tray startup, shortcut saving, and Linux-only settings
- Handle stat error in processRecording (missing file)
- Select correct binary name based on OS in updater
- Increase tray icon canvas size from 64 to 128 pixels
- Update frontend dist asset hash
This commit is contained in:
Your Name
2026-06-09 15:14:30 -07:00
parent 593a5941a1
commit ce49f66c89
29 changed files with 240 additions and 39 deletions
+2
View File
@@ -83,6 +83,8 @@ func (l *Listener) Start() {
}
if modOnly {
// WINDOWS-ONLY FEATURE: Modifier-only shortcuts (e.g. ctrl+win without a key)
// are only supported on Windows via modifier polling. On Linux, this is rejected.
if runtime.GOOS != "windows" {
logger.Error("Modifier-only shortcuts like %q are only supported on Windows", l.shortcut)
l.mu.Unlock()
+7
View File
@@ -1,4 +1,11 @@
//go:build linux
// ============================================================
// LINUX-ONLY FILE — Linux media playback management.
// Uses playerctl to pause/resume media during recording.
// The Windows equivalent is in internal/windows/
// ============================================================
package linux
import (
+7
View File
@@ -1,5 +1,12 @@
//go:build linux
// ============================================================
// LINUX-ONLY FILE — No-op overlay implementation for Linux.
// On Linux, the overlay is intentionally disabled (no desktop
// notifications). The Windows equivalent with full Win32 overlay
// is in internal/windows/overlay.go
// ============================================================
package linux
// linuxOverlay intentionally does not show system notifications. Tray status
+7
View File
@@ -1,4 +1,11 @@
//go:build linux
// ============================================================
// LINUX-ONLY FILE — Linux process management utilities.
// Uses syscall.Kill(pid, 0) to check if a process is running.
// The Windows equivalent is in internal/windows/process.go
// ============================================================
package linux
import "syscall"
+8
View File
@@ -1,4 +1,12 @@
//go:build linux
// ============================================================
// LINUX-ONLY FILE — Linux startup (autostart) management via
// XDG Desktop Entry files (.desktop). Creates/removes autostart
// entries and manages the application's .desktop file.
// The Windows equivalent is in internal/windows/
// ============================================================
package linux
import (
+8
View File
@@ -1,3 +1,11 @@
// ============================================================
// CROSS-PLATFORM FILE — This defines the Overlay interface
// that is implemented differently on each platform:
// - Windows: internal/windows/overlay.go (Win32 overlay)
// - Linux: internal/linux/overlay.go (no-op)
// This file itself is compiled on ALL platforms.
// ============================================================
package platform
// Overlay defines the cross-platform interface for screen overlays
+7
View File
@@ -1,4 +1,11 @@
//go:build linux
// ============================================================
// LINUX-ONLY FILE — Delegates all platform operations to the
// internal/linux package (overlay, media, startup, process).
// The Windows equivalent is platform_windows.go
// ============================================================
package platform
import "wis-free-v3/internal/linux"
+7
View File
@@ -1,4 +1,11 @@
//go:build windows
// ============================================================
// WINDOWS-ONLY FILE — Delegates all platform operations to the
// internal/windows package (overlay, media, startup, process).
// The Linux equivalent is platform_linux.go
// ============================================================
package platform
import "wis-free-v3/internal/windows"
+24 -7
View File
@@ -9,6 +9,7 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
@@ -115,7 +116,13 @@ func (m *Manager) Install(model string) error {
return fmt.Errorf("failed to create install directory: %w", err)
}
// Create install script
if runtime.GOOS == "linux" || runtime.GOOS == "darwin" {
// On Linux/macOS, the automatic installer is not available.
// Users should install whisper.cpp from their package manager or build from source.
return fmt.Errorf("automatic whisper installation is not available on %s; install whisper.cpp from your package manager (e.g. 'sudo apt install whisper-cpp' or 'brew install whisper-cpp') or build from source at https://github.com/ggerganov/whisper.cpp, then place the binary and model in %s", runtime.GOOS, m.installDir)
}
// Create install script (Windows batch file)
scriptPath := filepath.Join(m.installDir, "install.bat")
script := m.generateInstallScript(model)
if err := os.WriteFile(scriptPath, []byte(script), 0755); err != nil {
@@ -363,11 +370,18 @@ func (m *Manager) getBinaryPath() string {
// whisper.cpp extracts to a Release subdirectory
releaseDir := filepath.Join(m.installDir, "Release")
// Try different possible binary names
// whisper-cli.exe is the new standard (main.exe is deprecated)
// Try different possible binary names (platform-aware)
// On Windows: whisper-cli.exe / main.exe
// On Linux/macOS: whisper-cli / main
possibleNames := []string{
"whisper-cli.exe",
"main.exe",
"whisper-cli",
"main",
}
if runtime.GOOS == "windows" {
possibleNames = []string{
"whisper-cli.exe",
"main.exe",
}
}
// First check in Release subdirectory
@@ -386,8 +400,11 @@ func (m *Manager) getBinaryPath() string {
}
}
// Default to Release/main.exe
return filepath.Join(releaseDir, "main.exe")
// Default path
if runtime.GOOS == "windows" {
return filepath.Join(releaseDir, "main.exe")
}
return filepath.Join(releaseDir, "main")
}
// CheckOnline checks if internet is available
+28 -25
View File
@@ -279,52 +279,55 @@ func initDynamicIcons() {
}
func createMicPNG(c color.Color) []byte {
img := image.NewRGBA(image.Rect(0, 0, 64, 64))
const S = 128 // canvas size
img := image.NewRGBA(image.Rect(0, 0, S, S))
// All pixels are transparent by default (new RGBA starts with 0 alpha).
cx, cy := S/2, S/2 // center (64, 50)
// Let's draw the microphone parts
for y := 0; y < 64; y++ {
for x := 0; x < 64; x++ {
for y := 0; y < S; y++ {
for x := 0; x < S; x++ {
drawPixel := false
// 1. Capsule Body (Rounded Rectangle)
// Capsule center is X=32, Y=25. Width=14 (radius 7), height of straight part = 10 (Y from 20 to 30)
if x >= 25 && x <= 39 && y >= 20 && y <= 30 {
// Capsule center is X=64, Y=50. Width=28 (radius 14), height of straight part = 20 (Y from 40 to 60)
if x >= 50 && x <= 78 && y >= 40 && y <= 60 {
drawPixel = true
} else if y < 20 {
// Top cap: center (32, 20), radius 7
dx := float64(x - 32)
dy := float64(y - 20)
if dx*dx+dy*dy <= 49 { // 7^2
} else if y < 40 {
// Top cap: center (64, 40), radius 14
dx := float64(x - cx)
dy := float64(y - 40)
if dx*dx+dy*dy <= 196 { // 14^2
drawPixel = true
}
} else if y > 30 && y <= 37 {
// Bottom cap: center (32, 30), radius 7
dx := float64(x - 32)
dy := float64(y - 30)
if dx*dx+dy*dy <= 49 {
} else if y > 60 && y <= 74 {
// Bottom cap: center (64, 60), radius 14
dx := float64(x - cx)
dy := float64(y - 60)
if dx*dx+dy*dy <= 196 {
drawPixel = true
}
}
// 2. U-stand
// Center of U-stand circle is (32, 25).
// Outer radius = 15, inner radius = 12 (thickness 3)
// Only draw for Y >= 25 and Y <= 40
dx := float64(x - 32)
dy := float64(y - 25)
// Center of U-stand circle is (64, 50).
// Outer radius = 30, inner radius = 24 (thickness 6)
// Only draw for Y >= 50 and Y <= 80
dx := float64(x - cx)
dy := float64(y - cy)
distSq := dx*dx + dy*dy
if y >= 25 && y <= 40 && distSq >= 144 && distSq <= 225 { // 12^2 to 15^2
if y >= 50 && y <= 80 && distSq >= 576 && distSq <= 900 { // 24^2 to 30^2
drawPixel = true
}
// 3. Stem (Vertical line from Y=40 to 50, X=31 to 33)
if x >= 31 && x <= 33 && y >= 40 && y <= 50 {
// 3. Stem (Vertical line from Y=80 to 100, X=62 to 66)
if x >= 62 && x <= 66 && y >= 80 && y <= 100 {
drawPixel = true
}
// 4. Base (Horizontal line at Y=50 to 52, X=20 to 44)
if x >= 20 && x <= 44 && y >= 50 && y <= 52 {
// 4. Base (Horizontal line at Y=100 to 104, X=40 to 88)
if x >= 40 && x <= 88 && y >= 100 && y <= 104 {
drawPixel = true
}
+6
View File
@@ -1,4 +1,10 @@
//go:build linux
// ============================================================
// LINUX-ONLY FILE — Sets the GTK program name for the system tray
// on Linux. This is required for proper desktop integration.
// ============================================================
package tray
/*
+7
View File
@@ -1,5 +1,12 @@
//go:build linux
// ============================================================
// LINUX-ONLY FILE — Tray startup for Linux. On Linux, Wails'
// GTK main loop is already running, so we use systray.Register
// instead of systray.Run (which would start a second gtk_main).
// The Windows equivalent is tray_start_nonlinux.go
// ============================================================
package tray
import "github.com/getlantern/systray"
+7
View File
@@ -1,5 +1,12 @@
//go:build !linux
// ============================================================
// WINDOWS-ONLY FILE — Tray startup for Windows (and macOS).
// Uses systray.Run which blocks and runs the Win32 message pump.
// The Linux equivalent is tray_start_linux.go which uses
// systray.Register to integrate with the existing GTK loop.
// ============================================================
package tray
import (
+8
View File
@@ -1,4 +1,12 @@
//go:build windows
// ============================================================
// WINDOWS-ONLY FILE — Windows media playback management.
// Uses Win32 keybd_event to send VK_MEDIA_PLAY_PAUSE and a
// PowerShell script to check media playback state via SMTC.
// The Linux equivalent is internal/linux/media.go (playerctl)
// ============================================================
package windows
import (
+8
View File
@@ -1,4 +1,12 @@
//go:build windows
// ============================================================
// WINDOWS-ONLY FILE — Native Windows overlay window using
// Win32 API directly (CreateWindowEx, GDI drawing, etc.).
// This shows a pill-shaped overlay on screen during recording.
// The Linux equivalent is internal/linux/overlay.go (no-op).
// ============================================================
package windows
import (
+8
View File
@@ -1,4 +1,12 @@
//go:build windows
// ============================================================
// WINDOWS-ONLY FILE — Windows process management utilities.
// Uses Win32 API (OpenProcess, GetExitCodeProcess) to check
// if a process is running. The Linux equivalent for process
// checking is in internal/linux/process.go
// ============================================================
package windows
import (
+8
View File
@@ -1,4 +1,12 @@
//go:build windows
// ============================================================
// WINDOWS-ONLY FILE — Windows startup (autostart) management
// via the Windows Registry (HKCU\Software\Microsoft\Windows\
// CurrentVersion\Run). The Linux equivalent for autostart
// management is internal/linux/startup.go (XDG .desktop files)
// ============================================================
package windows
import (
+7
View File
@@ -1,5 +1,12 @@
//go:build linux
// ============================================================
// LINUX-ONLY FILE — Portal-based global hotkey implementation
// for Linux using the org.freedesktop.portal.GlobalShortcuts
// D-Bus API (Wayland/desktop-agnostic).
// The Windows equivalent is in hotkey_windows.go
// ============================================================
package hotkey
import (