refactor: implement cross-platform abstraction for process management, media control, and startup configuration (TESTING)

This commit is contained in:
jahruz67
2026-04-09 21:31:02 -07:00
parent cc644e3059
commit ba0e75c503
25 changed files with 458 additions and 48 deletions
+42
View File
@@ -0,0 +1,42 @@
//go:build linux
package linux
import (
"os/exec"
"wis-free-v3/internal/logger"
)
// IsPlaying checks if media is currently playing using playerctl
func IsPlaying() bool {
cmd := exec.Command("playerctl", "status")
out, err := cmd.Output()
if err != nil {
return false
}
return string(out) == "Playing\n"
}
// TogglePlayPause sends the play-pause command via playerctl
func TogglePlayPause() {
cmd := exec.Command("playerctl", "play-pause")
err := cmd.Run()
if err != nil {
logger.Error("Failed to toggle media on Linux: %v", err)
}
}
// PauseMedia checks if playing, pauses if so, returns whether we paused
func PauseMedia() bool {
wasPlaying := IsPlaying()
if wasPlaying {
TogglePlayPause()
}
return wasPlaying
}
// ResumeMedia resumes only if we paused it
func ResumeMedia(wasPaused bool) {
if wasPaused {
TogglePlayPause()
}
}
+15
View File
@@ -0,0 +1,15 @@
//go:build linux
package linux
// linuxOverlay provides a simple dummy implementation for Linux.
// It relies on tray icon status updates for visual feedback instead of raw drawing.
type linuxOverlay struct{}
func NewOverlay() *linuxOverlay {
return &linuxOverlay{}
}
func (o *linuxOverlay) Show(message string) {}
func (o *linuxOverlay) Hide() {}
func (o *linuxOverlay) SetVolume(level float64) {}
func (o *linuxOverlay) Close() {}
+11
View File
@@ -0,0 +1,11 @@
//go:build linux
package linux
import "syscall"
// IsProcessRunning checks if a process with the given PID exists on Linux.
func IsProcessRunning(pid int) bool {
// sending signal 0 checks if the process exists and we have permission
err := syscall.Kill(pid, 0)
return err == nil
}
+91
View File
@@ -0,0 +1,91 @@
//go:build linux
package linux
import (
"fmt"
"os"
"path/filepath"
)
const (
appName = "wis-free-v3"
desktopFileContent = `[Desktop Entry]
Type=Application
Name=WIS Free V3
Exec="%s"
Terminal=false
Categories=Utility;
X-GNOME-Autostart-enabled=true
`
)
func getAutostartPath() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
configDir := os.Getenv("XDG_CONFIG_HOME")
if configDir == "" {
configDir = filepath.Join(home, ".config")
}
autostartDir := filepath.Join(configDir, "autostart")
if err := os.MkdirAll(autostartDir, 0755); err != nil {
return "", err
}
return filepath.Join(autostartDir, appName+".desktop"), nil
}
func getExecutablePath() (string, error) {
exe, err := os.Executable()
if err != nil {
return "", err
}
return filepath.Abs(exe)
}
func AddToStartup() error {
autostartPath, err := getAutostartPath()
if err != nil {
return fmt.Errorf("failed to determine autostart path: %w", err)
}
exePath, err := getExecutablePath()
if err != nil {
return fmt.Errorf("failed to get executable path: %w", err)
}
content := fmt.Sprintf(desktopFileContent, exePath)
if err := os.WriteFile(autostartPath, []byte(content), 0644); err != nil {
return fmt.Errorf("failed to write autostart file: %w", err)
}
return nil
}
func RemoveFromStartup() error {
autostartPath, err := getAutostartPath()
if err != nil {
return fmt.Errorf("failed to determine autostart path: %w", err)
}
if err := os.Remove(autostartPath); err != nil {
if !os.IsNotExist(err) {
return fmt.Errorf("failed to remove autostart file: %w", err)
}
}
return nil
}
func IsInStartup() bool {
autostartPath, err := getAutostartPath()
if err != nil {
return false
}
_, err = os.Stat(autostartPath)
return err == nil
}
+9
View File
@@ -0,0 +1,9 @@
package platform
// Overlay defines the cross-platform interface for screen overlays
type Overlay interface {
Show(message string)
Hide()
SetVolume(level float64)
Close()
}
+32
View File
@@ -0,0 +1,32 @@
//go:build linux
package platform
import "wis-free-v3/internal/linux"
func NewOverlay() Overlay {
return linux.NewOverlay()
}
func PauseMedia() bool {
return linux.PauseMedia()
}
func ResumeMedia(wasPaused bool) {
linux.ResumeMedia(wasPaused)
}
func AddToStartup() error {
return linux.AddToStartup()
}
func RemoveFromStartup() error {
return linux.RemoveFromStartup()
}
func IsInStartup() bool {
return linux.IsInStartup()
}
func IsProcessRunning(pid int) bool {
return linux.IsProcessRunning(pid)
}
+32
View File
@@ -0,0 +1,32 @@
//go:build windows
package platform
import "wis-free-v3/internal/windows"
func NewOverlay() Overlay {
return windows.NewOverlay()
}
func PauseMedia() bool {
return windows.PauseMedia()
}
func ResumeMedia(wasPaused bool) {
windows.ResumeMedia(wasPaused)
}
func AddToStartup() error {
return windows.AddToStartup()
}
func RemoveFromStartup() error {
return windows.RemoveFromStartup()
}
func IsInStartup() bool {
return windows.IsInStartup()
}
func IsProcessRunning(pid int) bool {
return windows.IsProcessRunning(pid)
}
@@ -0,0 +1,9 @@
//go:build !windows
package whisper
import "os/exec"
func hideWindowContext(cmd *exec.Cmd) {
// Not needed on non-Windows platforms
}
+15
View File
@@ -0,0 +1,15 @@
//go:build windows
package whisper
import (
"os/exec"
"syscall"
)
func hideWindowContext(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
CreationFlags: 0x08000000, // CREATE_NO_WINDOW
}
}
+2 -6
View File
@@ -10,7 +10,6 @@ import (
"os/exec"
"path/filepath"
"strings"
"syscall"
"time"
"wis-free-v3/internal/logger"
@@ -285,11 +284,8 @@ func (m *Manager) Transcribe(audioPath string) (string, error) {
// Set working directory to the binary's location so it can find DLLs
cmd.Dir = filepath.Dir(binaryPath)
// Hide the console window
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
CreationFlags: 0x08000000, // CREATE_NO_WINDOW
}
// Hide the console window on Windows
hideWindowContext(cmd)
output, err := cmd.CombinedOutput()
outputStr := string(output)
+8 -8
View File
@@ -8,7 +8,7 @@ import (
"wis-free-v3/internal/config"
"wis-free-v3/internal/logger"
"wis-free-v3/internal/system/startup"
"wis-free-v3/internal/platform"
"github.com/getlantern/systray"
)
@@ -50,9 +50,9 @@ func onReady(app App) {
menuSettings := systray.AddMenuItem("Settings", "Open settings window")
menuStartup := systray.AddMenuItemCheckbox(
"Start with Windows",
"Automatically start when Windows boots",
startup.IsInStartup(),
"Start with system",
"Automatically start when computer boots",
platform.IsInStartup(),
)
systray.AddSeparator()
@@ -82,18 +82,18 @@ func handleMenuEvents(app App, settings, startupItem, exit *systray.MenuItem) {
// toggleStartup handles the startup toggle menu item.
func toggleStartup(item *systray.MenuItem) {
if item.Checked() {
if err := startup.RemoveFromStartup(); err != nil {
if err := platform.RemoveFromStartup(); err != nil {
logger.Error("Failed to remove from startup: %v", err)
} else {
item.Uncheck()
logger.Info("Removed from Windows startup")
logger.Info("Removed from system startup")
}
} else {
if err := startup.AddToStartup(); err != nil {
if err := platform.AddToStartup(); err != nil {
logger.Error("Failed to add to startup: %v", err)
} else {
item.Check()
logger.Info("Added to Windows startup")
logger.Info("Added to system startup")
}
}
}
@@ -1,4 +1,5 @@
package media
//go:build windows
package windows
import (
_ "embed"
@@ -10,8 +11,8 @@ import (
)
var (
user32 = syscall.NewLazyDLL("user32.dll")
keybd_event = user32.NewProc("keybd_event")
user32Media = syscall.NewLazyDLL("user32.dll")
keybd_event = user32Media.NewProc("keybd_event")
)
//go:embed check-media.ps1
@@ -1,4 +1,5 @@
package overlay
//go:build windows
package windows
import (
"math"
@@ -255,8 +256,17 @@ func (o *Overlay) run() {
procTranslateMessage.Call(uintptr(unsafe.Pointer(&msg)))
procDispatchMessage.Call(uintptr(unsafe.Pointer(&msg)))
} else {
// Higher resolution sleep for better responsiveness while keeping CPU low
time.Sleep(2 * time.Millisecond)
o.mu.RLock()
isShowing := o.isShowing
o.mu.RUnlock()
if isShowing {
// Higher resolution sleep for smooth animation when visible
time.Sleep(2 * time.Millisecond)
} else {
// Greatly reduce wakeups to save CPU when hidden (app is idle 99% of the time)
time.Sleep(50 * time.Millisecond)
}
}
}
}
+16
View File
@@ -0,0 +1,16 @@
//go:build windows
package windows
import "syscall"
// IsProcessRunning checks if a process with the given PID exists on Windows.
func IsProcessRunning(pid int) bool {
const PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
handle, err := syscall.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid))
if err != nil {
return false
}
syscall.CloseHandle(handle)
return true
}
@@ -1,6 +1,5 @@
// Package startup manages Windows startup registry entries for the application.
// It allows the application to automatically start when the user logs in.
package startup
//go:build windows
package windows
import (
"fmt"