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
-455
View File
@@ -1,455 +0,0 @@
package overlay
import (
"math"
"runtime"
"strings"
"sync"
"syscall"
"time"
"unsafe"
)
var (
user32 = syscall.NewLazyDLL("user32.dll")
gdi32 = syscall.NewLazyDLL("gdi32.dll")
procCreateWindowEx = user32.NewProc("CreateWindowExW")
procDefWindowProc = user32.NewProc("DefWindowProcW")
procDispatchMessage = user32.NewProc("DispatchMessageW")
procPeekMessage = user32.NewProc("PeekMessageW")
procRegisterClassEx = user32.NewProc("RegisterClassExW")
procTranslateMessage = user32.NewProc("TranslateMessage")
procShowWindow = user32.NewProc("ShowWindow")
procUpdateWindow = user32.NewProc("UpdateWindow")
procGetSystemMetrics = user32.NewProc("GetSystemMetrics")
procSetLayeredWindowAttributes = user32.NewProc("SetLayeredWindowAttributes")
procBeginPaint = user32.NewProc("BeginPaint")
procEndPaint = user32.NewProc("EndPaint")
procCreateSolidBrush = gdi32.NewProc("CreateSolidBrush")
procCreateFontW = gdi32.NewProc("CreateFontW")
procSelectObject = gdi32.NewProc("SelectObject")
procDeleteObject = gdi32.NewProc("DeleteObject")
procSetBkMode = gdi32.NewProc("SetBkMode")
procSetTextColor = gdi32.NewProc("SetTextColor")
procDrawTextW = user32.NewProc("DrawTextW")
procPostMessage = user32.NewProc("PostMessageW")
procInvalidateRect = user32.NewProc("InvalidateRect")
procLoadCursor = user32.NewProc("LoadCursorW")
procCreatePen = gdi32.NewProc("CreatePen")
procEllipse = gdi32.NewProc("Ellipse")
procCreateRoundRectRgn = gdi32.NewProc("CreateRoundRectRgn")
procSetWindowRgn = user32.NewProc("SetWindowRgn")
procFillRgn = gdi32.NewProc("FillRgn")
procCreateCompatibleDC = gdi32.NewProc("CreateCompatibleDC")
procCreateCompatibleBitmap = gdi32.NewProc("CreateCompatibleBitmap")
procBitBlt = gdi32.NewProc("BitBlt")
procDeleteDC = gdi32.NewProc("DeleteDC")
)
const (
WS_POPUP = 0x80000000
WS_EX_LAYERED = 0x00080000
WS_EX_TOPMOST = 0x00000008
WS_EX_TOOLWINDOW = 0x00000080
WS_EX_TRANSPARENT = 0x00000020
SW_SHOW = 5
SW_HIDE = 0
LWA_ALPHA = 0x00000002
SM_CXSCREEN = 0
SM_CYSCREEN = 1
TRANSPARENT_BK = 1
DT_CENTER = 0x00000001
DT_VCENTER = 0x00000004
DT_SINGLELINE = 0x00000020
WM_PAINT = 0x000F
WM_DESTROY = 0x0002
WM_CLOSE = 0x0010
IDC_ARROW = 32512
PM_REMOVE = 0x0001
PS_SOLID = 0
WM_ERASEBKGND = 0x0014
)
const (
COLOR_BG_DARK = 0x1A0F0B // Deep dark background (BGR for #0b0f1a)
COLOR_MIC_RED = 0x4545FF // Red for recording (BGR)
COLOR_MIC_GREEN = 0x50C850 // Green for ready
COLOR_MIC_ORANGE = 0x00A5FF // Orange for processing
COLOR_WHITE = 0xFFFFFF
COLOR_GRAY = 0x808080
)
// Overlay manages a transparent overlay window
type Overlay struct {
hwnd syscall.Handle
text string
isShowing bool
running bool
mu sync.RWMutex
stopCh chan struct{}
bgBrush syscall.Handle
hFont syscall.Handle
volume float64 // Current raw volume (0.0 - 1.0)
smoothedVolume float64 // Moving average volume for smoother animations
}
var globalOverlay *Overlay
// NewOverlay creates a new overlay instance
func NewOverlay() *Overlay {
o := &Overlay{
text: "Ready",
stopCh: make(chan struct{}),
}
globalOverlay = o
go o.run()
return o
}
// Show displays the overlay with the given message
func (o *Overlay) Show(message string) {
o.mu.Lock()
o.text = message
o.isShowing = true
o.mu.Unlock()
if o.hwnd != 0 {
procInvalidateRect.Call(uintptr(o.hwnd), 0, 1)
procShowWindow.Call(uintptr(o.hwnd), SW_SHOW)
}
}
// Hide hides the overlay
func (o *Overlay) Hide() {
o.mu.Lock()
o.isShowing = false
o.mu.Unlock()
if o.hwnd != 0 {
procShowWindow.Call(uintptr(o.hwnd), SW_HIDE)
}
}
// SetVolume updates the current audio volume level
func (o *Overlay) SetVolume(level float64) {
o.mu.Lock()
o.volume = level
// Stronger smoothing: 10% new value, 90% old value to reduce jitter/flicker
o.smoothedVolume = (o.smoothedVolume * 0.9) + (level * 0.1)
o.mu.Unlock()
}
// Close stops the overlay
func (o *Overlay) Close() {
if o.hwnd != 0 {
procPostMessage.Call(uintptr(o.hwnd), WM_CLOSE, 0, 0)
}
if o.bgBrush != 0 {
procDeleteObject.Call(uintptr(o.bgBrush))
}
if o.hFont != 0 {
procDeleteObject.Call(uintptr(o.hFont))
}
close(o.stopCh)
}
func (o *Overlay) run() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
o.running = true
defer func() { o.running = false }()
className := syscall.StringToUTF16Ptr("wis-free-v3Overlay")
var wc wndClassEx
wc.cbSize = uint32(unsafe.Sizeof(wc))
wc.lpfnWndProc = syscall.NewCallback(overlayWndProc)
wc.lpszClassName = className
// Pre-create resources
o.bgBrush = syscall.Handle(uintptr(0))
brushRec, _, _ := procCreateSolidBrush.Call(COLOR_BG_DARK)
o.bgBrush = syscall.Handle(brushRec)
fontName := syscall.StringToUTF16Ptr("Segoe UI")
fontRec, _, _ := procCreateFontW.Call(
18, 0, 0, 0, 600,
0, 0, 0, 0, 0, 0, 0, 0,
uintptr(unsafe.Pointer(fontName)),
)
o.hFont = syscall.Handle(fontRec)
cursor, _, _ := procLoadCursor.Call(0, uintptr(IDC_ARROW))
wc.hCursor = syscall.Handle(cursor)
wc.hbrBackground = o.bgBrush
procRegisterClassEx.Call(uintptr(unsafe.Pointer(&wc)))
// Get screen dimensions
screenWidth, _, _ := procGetSystemMetrics.Call(SM_CXSCREEN)
screenHeight, _, _ := procGetSystemMetrics.Call(SM_CYSCREEN)
// Pill dimensions
width := 120
height := 46
x := (int(screenWidth) - width) / 2
y := int(screenHeight) - height - 80 // 80px from bottom
hwnd, _, _ := procCreateWindowEx.Call(
WS_EX_LAYERED|WS_EX_TOPMOST|WS_EX_TOOLWINDOW|WS_EX_TRANSPARENT,
uintptr(unsafe.Pointer(className)),
0,
WS_POPUP,
uintptr(x), uintptr(y), uintptr(width), uintptr(height),
0, 0, 0, 0,
)
if hwnd == 0 {
return
}
o.hwnd = syscall.Handle(hwnd)
// Create a rounded region to clip the window (true pill shape, no corners)
// The corner radius should be half the height for a perfect pill
rgn, _, _ := procCreateRoundRectRgn.Call(0, 0, uintptr(width+1), uintptr(height+1), uintptr(height), uintptr(height))
procSetWindowRgn.Call(hwnd, rgn, 1)
// Set window transparency
procSetLayeredWindowAttributes.Call(uintptr(hwnd), 0, 240, LWA_ALPHA)
procShowWindow.Call(uintptr(hwnd), SW_HIDE)
procUpdateWindow.Call(uintptr(hwnd))
// Message loop
ticker := time.NewTicker(16 * time.Millisecond) // ~60 FPS
defer ticker.Stop()
var msg msg
for {
select {
case <-o.stopCh:
return
case <-ticker.C:
if o.hwnd != 0 && o.isShowing {
// Only invalidate if we are in a state that needs animation
o.mu.RLock()
needsAnimation := strings.HasPrefix(o.text, "Recording") || strings.HasPrefix(o.text, "Transcribing") || strings.HasPrefix(o.text, "Processing")
o.mu.RUnlock()
if needsAnimation {
procInvalidateRect.Call(uintptr(o.hwnd), 0, 0)
}
}
default:
ret, _, _ := procPeekMessage.Call(
uintptr(unsafe.Pointer(&msg)),
0, 0, 0,
PM_REMOVE,
)
if ret != 0 {
if msg.message == WM_CLOSE {
return
}
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)
}
}
}
}
func overlayWndProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr {
switch msg {
case WM_ERASEBKGND:
return 1 // Prevent white flash by handling erase ourselves
case WM_PAINT:
var ps paintStruct
hdc, _, _ := procBeginPaint.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&ps)))
// Double buffering: Create a memory DC to draw off-screen first
memDC, _, _ := procCreateCompatibleDC.Call(hdc)
memBitmap, _, _ := procCreateCompatibleBitmap.Call(hdc, 120, 46)
oldBitmap, _, _ := procSelectObject.Call(memDC, memBitmap)
// Access global state safely
var text string
var bgBrush syscall.Handle
var hFont syscall.Handle
var volume float64
if globalOverlay != nil {
globalOverlay.mu.RLock()
text = globalOverlay.text
bgBrush = globalOverlay.bgBrush
hFont = globalOverlay.hFont
volume = globalOverlay.smoothedVolume
globalOverlay.mu.RUnlock()
}
// 1. Clear memory DC with background
rgn, _, _ := procCreateRoundRectRgn.Call(0, 0, 121, 47, 46, 46)
if bgBrush != 0 {
procFillRgn.Call(memDC, rgn, uintptr(bgBrush))
}
procDeleteObject.Call(rgn)
// 2. Draw content to memory DC
if strings.HasPrefix(text, "Recording") {
// Draw animated waves for recording
// Slowed down from 10.0 to 6.0
t := float64(time.Now().UnixNano()) / 1e9 * 6.0
barCount := 7 // Reduced from 9 for shorter pill
barWidth := 4
barGap := 4
totalWidth := barCount*barWidth + (barCount-1)*barGap
startX := (120 - totalWidth) / 2
centerY := 46 / 2
// Use colors for waves? The user said "instead of... the color... it's just like waves"
// But maybe a subtle red pulse is nice? Let's use WHITE as requested.
barBrush, _, _ := procCreateSolidBrush.Call(uintptr(COLOR_WHITE))
for i := 0; i < barCount; i++ {
// Different phases for each bar
phase := float64(i) * 0.8
// Base height from animation
animH := 4.0 * math.Abs(math.Sin(t+phase))
// Scale height based on real-time volume
// Boosted sensitivity from 40.0 to 120.0 for better response to normal speech
volH := volume * 120.0
// Total height: slow background wave + responsive voice spikes
h := 6.0 + animH + volH
// Cap height to 38 (max pill center space)
if h > 38 {
h = 38
}
x := int32(startX + i*(barWidth+barGap))
y1 := int32(float64(centerY) - h/2)
y2 := int32(float64(centerY) + h/2)
barRgn, _, _ := procCreateRoundRectRgn.Call(uintptr(x), uintptr(y1), uintptr(x+int32(barWidth)), uintptr(y2), 4, 4)
procFillRgn.Call(memDC, barRgn, barBrush)
procDeleteObject.Call(barRgn)
}
procDeleteObject.Call(barBrush)
} else if strings.HasPrefix(text, "Transcribing") || strings.HasPrefix(text, "Processing") {
// Different animation for processing/transcribing (more uniform, pulsing)
// Slowed down from 5.0 to 3.0
t := float64(time.Now().UnixNano()) / 1e9 * 3.0
barCount := 5 // Reduced from 7 for shorter pill
barWidth := 4
barGap := 6
totalWidth := barCount*barWidth + (barCount-1)*barGap
startX := (120 - totalWidth) / 2
centerY := 46 / 2
barBrush, _, _ := procCreateSolidBrush.Call(uintptr(COLOR_MIC_ORANGE)) // Orange for processing
for i := 0; i < barCount; i++ {
// Subtler oscillation
h := 10.0 + 10.0*math.Abs(math.Sin(t+float64(i)*0.3))
x := int32(startX + i*(barWidth+barGap))
y1 := int32(float64(centerY) - h/2)
y2 := int32(float64(centerY) + h/2)
barRgn, _, _ := procCreateRoundRectRgn.Call(uintptr(x), uintptr(y1), uintptr(x+int32(barWidth)), uintptr(y2), 4, 4)
procFillRgn.Call(memDC, barRgn, barBrush)
procDeleteObject.Call(barRgn)
}
procDeleteObject.Call(barBrush)
} else {
// Show text for errors or "Ready" (though Ready is rarely shown in overlay)
// This keeps the user informed if something went wrong
procSetBkMode.Call(memDC, TRANSPARENT_BK)
textColor := COLOR_WHITE
if text == "Error" || strings.Contains(text, "Key") || strings.Contains(text, "Err") {
textColor = COLOR_MIC_RED
}
procSetTextColor.Call(memDC, uintptr(textColor))
if hFont != 0 {
oldFont, _, _ := procSelectObject.Call(memDC, uintptr(hFont))
// Centered text across the whole pill since there's no circle anymore
textRect := rect{0, 0, 120, 46}
textPtr := syscall.StringToUTF16Ptr(text)
procDrawTextW.Call(memDC, uintptr(unsafe.Pointer(textPtr)), ^uintptr(0), uintptr(unsafe.Pointer(&textRect)), DT_CENTER|DT_VCENTER|DT_SINGLELINE)
procSelectObject.Call(memDC, oldFont)
}
}
// 3. Copy everything from memory DC to screen (prevents flicker)
procBitBlt.Call(hdc, 0, 0, 120, 46, memDC, 0, 0, 0x00CC0020) // SRCCOPY
// Cleanup memory DC
procSelectObject.Call(memDC, oldBitmap)
procDeleteObject.Call(memBitmap)
procDeleteDC.Call(memDC)
procEndPaint.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&ps)))
return 0
case WM_DESTROY:
return 0
}
ret, _, _ := procDefWindowProc.Call(uintptr(hwnd), uintptr(msg), wParam, lParam)
return ret
}
// Structs for Windows API
type wndClassEx struct {
cbSize uint32
style uint32
lpfnWndProc uintptr
cbClsExtra int32
cbWndExtra int32
hInstance syscall.Handle
hIcon syscall.Handle
hCursor syscall.Handle
hbrBackground syscall.Handle
lpszMenuName *uint16
lpszClassName *uint16
hIconSm syscall.Handle
}
type msg struct {
hwnd syscall.Handle
message uint32
wParam uintptr
lParam uintptr
time uint32
pt point
}
type point struct {
x, y int32
}
type paintStruct struct {
hdc syscall.Handle
fErase int32
rcPaint rect
fRestore int32
fIncUpdate int32
rgbReserved [32]byte
}
type rect struct {
left, top, right, bottom int32
}
+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")
}
}
}