feat: add portable Windows zone manager
This commit is contained in:
@@ -0,0 +1,694 @@
|
||||
//go:build windows
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/stevec/fancywin/internal/config"
|
||||
"github.com/stevec/fancywin/internal/layout"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
eventSystemMoveSizeStart = 0x000A
|
||||
eventSystemMoveSizeEnd = 0x000B
|
||||
wineventOutOfContext = 0x0000
|
||||
wineventSkipOwnProcess = 0x0002
|
||||
objectIDWindow = 0
|
||||
wmHotkey = 0x0312
|
||||
wmTimer = 0x0113
|
||||
wmQuit = 0x0012
|
||||
monitorDefaultToNearest = 2
|
||||
monitorDefaultToNull = 0
|
||||
modAlt = 0x0001
|
||||
modControl = 0x0002
|
||||
modShift = 0x0004
|
||||
modWin = 0x0008
|
||||
modNoRepeat = 0x4000
|
||||
vkLButton = 0x01
|
||||
vkShift = 0x10
|
||||
gaRoot = 2
|
||||
gwOwner = 4
|
||||
wsChild = 0x40000000
|
||||
swRestore = 9
|
||||
swpNoZOrder = 0x0004
|
||||
swpNoActivate = 0x0010
|
||||
swpNoOwnerZOrder = 0x0200
|
||||
)
|
||||
|
||||
var (
|
||||
user32 = windows.NewLazySystemDLL("user32.dll")
|
||||
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||
dwmapi = windows.NewLazySystemDLL("dwmapi.dll")
|
||||
procSetWinEventHook = user32.NewProc("SetWinEventHook")
|
||||
procUnhookWinEvent = user32.NewProc("UnhookWinEvent")
|
||||
procGetMessage = user32.NewProc("GetMessageW")
|
||||
procTranslateMessage = user32.NewProc("TranslateMessage")
|
||||
procDispatchMessage = user32.NewProc("DispatchMessageW")
|
||||
procRegisterHotKey = user32.NewProc("RegisterHotKey")
|
||||
procUnregisterHotKey = user32.NewProc("UnregisterHotKey")
|
||||
procGetForegroundWindow = user32.NewProc("GetForegroundWindow")
|
||||
procGetCursorPos = user32.NewProc("GetCursorPos")
|
||||
procWindowFromPoint = user32.NewProc("WindowFromPoint")
|
||||
procMonitorFromPoint = user32.NewProc("MonitorFromPoint")
|
||||
procMonitorFromWindow = user32.NewProc("MonitorFromWindow")
|
||||
procGetMonitorInfo = user32.NewProc("GetMonitorInfoW")
|
||||
procGetAsyncKeyState = user32.NewProc("GetAsyncKeyState")
|
||||
procSetWindowPos = user32.NewProc("SetWindowPos")
|
||||
procShowWindow = user32.NewProc("ShowWindow")
|
||||
procIsWindowVisible = user32.NewProc("IsWindowVisible")
|
||||
procGetAncestor = user32.NewProc("GetAncestor")
|
||||
procGetWindow = user32.NewProc("GetWindow")
|
||||
procGetWindowLongPtr = user32.NewProc("GetWindowLongPtrW")
|
||||
procGetWindowRect = user32.NewProc("GetWindowRect")
|
||||
procGetWindowThreadProcessID = user32.NewProc("GetWindowThreadProcessId")
|
||||
procSetTimer = user32.NewProc("SetTimer")
|
||||
procKillTimer = user32.NewProc("KillTimer")
|
||||
procSetProcessDPIAwareV2 = user32.NewProc("SetProcessDpiAwarenessContext")
|
||||
procEnumDisplayMonitors = user32.NewProc("EnumDisplayMonitors")
|
||||
procCreateMutex = kernel32.NewProc("CreateMutexW")
|
||||
procDwmGetWindowAttribute = dwmapi.NewProc("DwmGetWindowAttribute")
|
||||
)
|
||||
|
||||
type point struct{ X, Y int32 }
|
||||
type rect struct{ Left, Top, Right, Bottom int32 }
|
||||
type message struct {
|
||||
HWnd windows.Handle
|
||||
Message uint32
|
||||
_ uint32
|
||||
WParam uintptr
|
||||
LParam uintptr
|
||||
Time uint32
|
||||
Pt point
|
||||
Private uint32
|
||||
}
|
||||
type monitorInfoEx struct {
|
||||
Size uint32
|
||||
Monitor rect
|
||||
Work rect
|
||||
Flags uint32
|
||||
Device [32]uint16
|
||||
}
|
||||
type hotkeyBinding struct {
|
||||
id int
|
||||
action string
|
||||
keys string
|
||||
}
|
||||
type engine struct {
|
||||
mu sync.RWMutex
|
||||
configPath string
|
||||
cfg config.Config
|
||||
configTime time.Time
|
||||
dragging windows.Handle
|
||||
dragShift bool
|
||||
mouseDown bool
|
||||
mouseWindow windows.Handle
|
||||
mouseStart rect
|
||||
mouseMoved bool
|
||||
mouseShift bool
|
||||
fallbackHandled windows.Handle
|
||||
hook windows.Handle
|
||||
hotkeys map[int]hotkeyBinding
|
||||
debug bool
|
||||
overlay *overlayManager
|
||||
overlayFailed bool
|
||||
}
|
||||
|
||||
var activeEngine *engine
|
||||
|
||||
func Run(configPath string, cfg config.Config, debug bool) error {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
procSetProcessDPIAwareV2.Call(^uintptr(3)) // DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 (-4)
|
||||
|
||||
name, _ := windows.UTF16PtrFromString("Local\\FancyWin-Portable-Window-Manager")
|
||||
mutex, _, createErr := procCreateMutex.Call(0, 0, uintptr(unsafe.Pointer(name)))
|
||||
if mutex == 0 {
|
||||
return fmt.Errorf("create single-instance mutex: %v", createErr)
|
||||
}
|
||||
defer windows.CloseHandle(windows.Handle(mutex))
|
||||
if createErr == windows.ERROR_ALREADY_EXISTS {
|
||||
return fmt.Errorf("another instance is already running")
|
||||
}
|
||||
|
||||
e := &engine{configPath: configPath, cfg: cfg, hotkeys: make(map[int]hotkeyBinding), debug: debug, overlay: &overlayManager{}}
|
||||
defer e.overlay.hide()
|
||||
e.rememberConfigTime()
|
||||
activeEngine = e
|
||||
defer func() { activeEngine = nil }()
|
||||
|
||||
callback := windows.NewCallback(winEventCallback)
|
||||
hook, _, callErr := procSetWinEventHook.Call(eventSystemMoveSizeStart, eventSystemMoveSizeEnd, 0, callback, 0, 0, wineventOutOfContext|wineventSkipOwnProcess)
|
||||
if hook == 0 {
|
||||
return fmt.Errorf("install window move hook: %v", callErr)
|
||||
}
|
||||
e.hook = windows.Handle(hook)
|
||||
defer procUnhookWinEvent.Call(hook)
|
||||
if err := e.registerHotkeys(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer e.unregisterHotkeys()
|
||||
configTimer, _, configTimerErr := procSetTimer.Call(0, 1, 2000, 0)
|
||||
if configTimer == 0 {
|
||||
return fmt.Errorf("create configuration timer: %v", configTimerErr)
|
||||
}
|
||||
defer procKillTimer.Call(0, configTimer)
|
||||
shiftTimer, _, shiftTimerErr := procSetTimer.Call(0, 2, 25, 0)
|
||||
if shiftTimer == 0 {
|
||||
return fmt.Errorf("create drag activation timer: %v", shiftTimerErr)
|
||||
}
|
||||
defer procKillTimer.Call(0, shiftTimer)
|
||||
|
||||
log.Printf("ready: Shift+drag snapping=%v, overlay=%v, %d hotkeys", cfg.ShiftDrag, cfg.Overlay.Enabled, len(e.hotkeys))
|
||||
logMonitors()
|
||||
var msg message
|
||||
for {
|
||||
r, _, err := procGetMessage.Call(uintptr(unsafe.Pointer(&msg)), 0, 0, 0)
|
||||
if int32(r) == -1 {
|
||||
return fmt.Errorf("message loop: %v", err)
|
||||
}
|
||||
if r == 0 || msg.Message == wmQuit {
|
||||
return nil
|
||||
}
|
||||
switch msg.Message {
|
||||
case wmHotkey:
|
||||
e.handleHotkey(int(msg.WParam))
|
||||
case wmTimer:
|
||||
switch msg.WParam {
|
||||
case configTimer:
|
||||
e.reloadIfChanged()
|
||||
case shiftTimer:
|
||||
e.pollInput()
|
||||
}
|
||||
}
|
||||
procTranslateMessage.Call(uintptr(unsafe.Pointer(&msg)))
|
||||
procDispatchMessage.Call(uintptr(unsafe.Pointer(&msg)))
|
||||
}
|
||||
}
|
||||
|
||||
func winEventCallback(_, event, rawHWND, idObject, idChild, _, _ uintptr) uintptr {
|
||||
e := activeEngine
|
||||
hwnd := windows.Handle(rawHWND)
|
||||
if e == nil || hwnd == 0 || idObject != objectIDWindow || idChild != 0 {
|
||||
return 0
|
||||
}
|
||||
switch event {
|
||||
case eventSystemMoveSizeStart:
|
||||
if e.canManage(hwnd) {
|
||||
e.fallbackHandled = 0
|
||||
e.dragging = hwnd
|
||||
e.dragShift = keyDown(vkShift)
|
||||
e.debugf("move started: hwnd=0x%x app=%q shift=%v", uintptr(hwnd), executableForWindow(hwnd), e.dragShift)
|
||||
} else {
|
||||
e.debugf("move ignored: hwnd=0x%x app=%q is not a manageable top-level window", uintptr(hwnd), executableForWindow(hwnd))
|
||||
}
|
||||
case eventSystemMoveSizeEnd:
|
||||
if e.dragging == hwnd {
|
||||
if e.fallbackHandled == hwnd {
|
||||
e.dragging = 0
|
||||
e.dragShift = false
|
||||
e.fallbackHandled = 0
|
||||
e.debugf("native move end arrived after mouse fallback: hwnd=0x%x", uintptr(hwnd))
|
||||
break
|
||||
}
|
||||
activated := e.dragShift || keyDown(vkShift)
|
||||
e.dragging = 0
|
||||
e.dragShift = false
|
||||
if e.mouseWindow == hwnd {
|
||||
e.mouseWindow = 0 // The native event path owns this drag.
|
||||
e.mouseMoved = false
|
||||
}
|
||||
e.mu.RLock()
|
||||
enabled := e.cfg.ShiftDrag
|
||||
e.mu.RUnlock()
|
||||
if enabled && activated {
|
||||
if ok, reason := e.snapAtCursor(hwnd); !ok {
|
||||
log.Printf("window was not snapped: %s", reason)
|
||||
} else {
|
||||
e.debugf("window snapped: hwnd=0x%x", uintptr(hwnd))
|
||||
}
|
||||
} else {
|
||||
e.debugf("move ended without snapping: hwnd=0x%x shift_drag=%v shift=%v", uintptr(hwnd), enabled, activated)
|
||||
}
|
||||
} else {
|
||||
e.debugf("move end ignored: hwnd=0x%x did not match active drag 0x%x", uintptr(hwnd), uintptr(e.dragging))
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (e *engine) pollInput() {
|
||||
if e.dragging != 0 {
|
||||
e.dragShift = keyDown(vkShift)
|
||||
}
|
||||
|
||||
down := keyDown(vkLButton)
|
||||
switch {
|
||||
case down && !e.mouseDown:
|
||||
e.beginMouseTracking()
|
||||
case down && e.mouseDown:
|
||||
e.updateMouseTracking()
|
||||
case !down && e.mouseDown:
|
||||
e.endMouseTracking()
|
||||
}
|
||||
e.mouseDown = down
|
||||
e.updateOverlay()
|
||||
}
|
||||
|
||||
func (e *engine) updateOverlay() {
|
||||
hide := func() {
|
||||
if e.overlay.hwnd != 0 {
|
||||
e.debugf("zone overlay hidden")
|
||||
}
|
||||
e.overlay.hide()
|
||||
}
|
||||
e.mu.RLock()
|
||||
cfg := e.cfg
|
||||
e.mu.RUnlock()
|
||||
activeDrag := e.mouseDown && (e.dragging != 0 || (e.mouseWindow != 0 && e.mouseMoved))
|
||||
if !cfg.Overlay.Enabled || !activeDrag || !keyDown(vkShift) {
|
||||
hide()
|
||||
return
|
||||
}
|
||||
var p point
|
||||
if ok, _, _ := procGetCursorPos.Call(uintptr(unsafe.Pointer(&p))); ok == 0 {
|
||||
hide()
|
||||
return
|
||||
}
|
||||
mon, _, _ := procMonitorFromPoint.Call(packPoint(p), monitorDefaultToNull)
|
||||
if mon == 0 {
|
||||
hide()
|
||||
return
|
||||
}
|
||||
mi, ok := monitorDetails(windows.Handle(mon))
|
||||
if !ok {
|
||||
hide()
|
||||
return
|
||||
}
|
||||
selected, ok := cfg.LayoutFor(mi.device())
|
||||
if !ok {
|
||||
hide()
|
||||
return
|
||||
}
|
||||
wasVisible := e.overlay.hwnd != 0
|
||||
if err := e.overlay.show(windows.Handle(mon), toLayoutRect(mi.Work), selected.Zones, cfg.Gap, cfg.Overlay); err != nil {
|
||||
hide()
|
||||
if !e.overlayFailed {
|
||||
log.Printf("cannot show zone overlay: %v", err)
|
||||
e.overlayFailed = true
|
||||
}
|
||||
} else if !wasVisible {
|
||||
e.debugf("zone overlay shown: monitor=%s zones=%d", mi.device(), len(selected.Zones))
|
||||
}
|
||||
}
|
||||
|
||||
func (e *engine) beginMouseTracking() {
|
||||
var p point
|
||||
if ok, _, _ := procGetCursorPos.Call(uintptr(unsafe.Pointer(&p))); ok == 0 {
|
||||
return
|
||||
}
|
||||
raw, _, _ := procWindowFromPoint.Call(packPoint(p))
|
||||
if raw == 0 {
|
||||
e.debugf("mouse fallback: no window under press at (%d,%d)", p.X, p.Y)
|
||||
return
|
||||
}
|
||||
root, _, _ := procGetAncestor.Call(raw, gaRoot)
|
||||
if root == 0 {
|
||||
root = raw
|
||||
}
|
||||
hwnd := windows.Handle(root)
|
||||
if !e.canManage(hwnd) {
|
||||
e.debugf("mouse fallback: window under press is not manageable: hwnd=0x%x app=%q", uintptr(hwnd), executableForWindow(hwnd))
|
||||
return
|
||||
}
|
||||
var start rect
|
||||
if ok, _, _ := procGetWindowRect.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&start))); ok == 0 {
|
||||
e.debugf("mouse fallback: GetWindowRect failed for hwnd=0x%x", uintptr(hwnd))
|
||||
return
|
||||
}
|
||||
e.mouseWindow = hwnd
|
||||
e.mouseStart = start
|
||||
e.mouseMoved = false
|
||||
e.mouseShift = keyDown(vkShift)
|
||||
e.debugf("mouse fallback tracking started: hwnd=0x%x app=%q shift=%v", uintptr(hwnd), executableForWindow(hwnd), e.mouseShift)
|
||||
}
|
||||
|
||||
func (e *engine) updateMouseTracking() {
|
||||
if e.mouseWindow == 0 {
|
||||
return
|
||||
}
|
||||
e.mouseShift = keyDown(vkShift)
|
||||
var current rect
|
||||
if ok, _, _ := procGetWindowRect.Call(uintptr(e.mouseWindow), uintptr(unsafe.Pointer(¤t))); ok != 0 && rectMoved(e.mouseStart, current) {
|
||||
e.mouseMoved = true
|
||||
}
|
||||
}
|
||||
|
||||
func (e *engine) endMouseTracking() {
|
||||
hwnd := e.mouseWindow
|
||||
moved := e.mouseMoved
|
||||
activated := e.mouseShift || keyDown(vkShift)
|
||||
e.mouseWindow = 0
|
||||
e.mouseMoved = false
|
||||
e.mouseShift = false
|
||||
if hwnd == 0 {
|
||||
return
|
||||
}
|
||||
if !moved {
|
||||
e.debugf("mouse fallback ignored click: hwnd=0x%x rectangle did not move", uintptr(hwnd))
|
||||
return
|
||||
}
|
||||
e.mu.RLock()
|
||||
enabled := e.cfg.ShiftDrag
|
||||
e.mu.RUnlock()
|
||||
if !enabled || !activated {
|
||||
e.debugf("mouse fallback move ended without snapping: hwnd=0x%x shift_drag=%v shift=%v", uintptr(hwnd), enabled, activated)
|
||||
return
|
||||
}
|
||||
if e.dragging == hwnd {
|
||||
e.fallbackHandled = hwnd
|
||||
}
|
||||
if ok, reason := e.snapAtCursor(hwnd); !ok {
|
||||
log.Printf("window was not snapped by mouse fallback: %s", reason)
|
||||
} else {
|
||||
e.debugf("window snapped by mouse fallback: hwnd=0x%x", uintptr(hwnd))
|
||||
}
|
||||
}
|
||||
|
||||
func rectMoved(a, b rect) bool {
|
||||
const threshold = int32(2)
|
||||
return abs32(a.Left-b.Left) > threshold || abs32(a.Top-b.Top) > threshold ||
|
||||
abs32(a.Right-b.Right) > threshold || abs32(a.Bottom-b.Bottom) > threshold
|
||||
}
|
||||
|
||||
func abs32(v int32) int32 {
|
||||
if v < 0 {
|
||||
return -v
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func packPoint(p point) uintptr {
|
||||
return uintptr(uint32(p.X)) | uintptr(uint64(uint32(p.Y))<<32)
|
||||
}
|
||||
|
||||
func (e *engine) snapAtCursor(hwnd windows.Handle) (bool, string) {
|
||||
var p point
|
||||
if ok, _, _ := procGetCursorPos.Call(uintptr(unsafe.Pointer(&p))); ok == 0 {
|
||||
return false, "GetCursorPos failed"
|
||||
}
|
||||
mon, _, _ := procMonitorFromPoint.Call(packPoint(p), monitorDefaultToNull)
|
||||
if mon == 0 {
|
||||
return false, fmt.Sprintf("no monitor contains pointer (%d,%d)", p.X, p.Y)
|
||||
}
|
||||
mi, ok := monitorDetails(windows.Handle(mon))
|
||||
if !ok {
|
||||
return false, "GetMonitorInfo failed"
|
||||
}
|
||||
e.mu.RLock()
|
||||
cfg := e.cfg
|
||||
e.mu.RUnlock()
|
||||
l, ok := cfg.LayoutFor(mi.device())
|
||||
if !ok {
|
||||
return false, fmt.Sprintf("no layout matches monitor %s and no wildcard layout exists", mi.device())
|
||||
}
|
||||
work := toLayoutRect(mi.Work)
|
||||
zi := layout.ZoneAt(work, l.Zones, 0, layout.Point{X: p.X, Y: p.Y})
|
||||
if zi < 0 {
|
||||
return false, fmt.Sprintf("pointer (%d,%d) is outside every zone on %s", p.X, p.Y, mi.device())
|
||||
}
|
||||
e.debugf("snap target: monitor=%s zone=%d name=%q pointer=(%d,%d)", mi.device(), zi+1, l.Zones[zi].Name, p.X, p.Y)
|
||||
if !e.moveTo(hwnd, work, l.Zones[zi], cfg.Gap) {
|
||||
return false, "SetWindowPos failed (the window may be elevated or may reject resizing)"
|
||||
}
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func (e *engine) handleHotkey(id int) {
|
||||
binding, ok := e.hotkeys[id]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
h, _, _ := procGetForegroundWindow.Call()
|
||||
hwnd := windows.Handle(h)
|
||||
if !e.canManage(hwnd) {
|
||||
return
|
||||
}
|
||||
mon, _, _ := procMonitorFromWindow.Call(uintptr(hwnd), monitorDefaultToNearest)
|
||||
if mon == 0 {
|
||||
return
|
||||
}
|
||||
mi, ok := monitorDetails(windows.Handle(mon))
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
e.mu.RLock()
|
||||
cfg := e.cfg
|
||||
e.mu.RUnlock()
|
||||
l, ok := cfg.LayoutFor(mi.device())
|
||||
if !ok || len(l.Zones) == 0 {
|
||||
return
|
||||
}
|
||||
idx := -1
|
||||
action := strings.ToLower(strings.TrimSpace(binding.action))
|
||||
if strings.HasPrefix(action, "zone_") {
|
||||
n, err := strconv.Atoi(strings.TrimPrefix(action, "zone_"))
|
||||
if err == nil {
|
||||
idx = n - 1
|
||||
}
|
||||
} else {
|
||||
var wr rect
|
||||
if r, _, _ := procGetWindowRect.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&wr))); r == 0 {
|
||||
return
|
||||
}
|
||||
center := layout.Point{X: (wr.Left + wr.Right) / 2, Y: (wr.Top + wr.Bottom) / 2}
|
||||
current := layout.Closest(toLayoutRect(mi.Work), l.Zones, center)
|
||||
if action == "next_zone" {
|
||||
idx = (current + 1) % len(l.Zones)
|
||||
} else {
|
||||
idx = (current - 1 + len(l.Zones)) % len(l.Zones)
|
||||
}
|
||||
}
|
||||
if idx >= 0 && idx < len(l.Zones) {
|
||||
e.moveTo(hwnd, toLayoutRect(mi.Work), l.Zones[idx], cfg.Gap)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *engine) moveTo(hwnd windows.Handle, work layout.Rect, z config.Zone, gap int) bool {
|
||||
r := layout.Resolve(work, z, gap)
|
||||
procShowWindow.Call(uintptr(hwnd), swRestore)
|
||||
r = outerRectForVisibleFrame(hwnd, r)
|
||||
ok, _, err := procSetWindowPos.Call(uintptr(hwnd), 0, uintptr(r.Left), uintptr(r.Top), uintptr(r.Right-r.Left), uintptr(r.Bottom-r.Top), swpNoZOrder|swpNoActivate|swpNoOwnerZOrder)
|
||||
if ok == 0 {
|
||||
log.Printf("cannot move window 0x%x: %v (it may be elevated)", uintptr(hwnd), err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (e *engine) debugf(format string, args ...any) {
|
||||
if e.debug {
|
||||
log.Printf("debug: "+format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Windows 11 windows have an invisible resize border outside the DWM-rendered
|
||||
// frame. Expand the SetWindowPos rectangle so the visible frame, rather than
|
||||
// that hidden border, aligns with the configured zone.
|
||||
func outerRectForVisibleFrame(hwnd windows.Handle, target layout.Rect) layout.Rect {
|
||||
var outer, frame rect
|
||||
if ok, _, _ := procGetWindowRect.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&outer))); ok == 0 {
|
||||
return target
|
||||
}
|
||||
const dwmwaExtendedFrameBounds = 9
|
||||
hr, _, _ := procDwmGetWindowAttribute.Call(uintptr(hwnd), dwmwaExtendedFrameBounds, uintptr(unsafe.Pointer(&frame)), unsafe.Sizeof(frame))
|
||||
if int32(hr) < 0 {
|
||||
return target
|
||||
}
|
||||
return layout.Rect{
|
||||
Left: target.Left - (frame.Left - outer.Left),
|
||||
Top: target.Top - (frame.Top - outer.Top),
|
||||
Right: target.Right + (outer.Right - frame.Right),
|
||||
Bottom: target.Bottom + (outer.Bottom - frame.Bottom),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *engine) canManage(hwnd windows.Handle) bool {
|
||||
if hwnd == 0 {
|
||||
return false
|
||||
}
|
||||
visible, _, _ := procIsWindowVisible.Call(uintptr(hwnd))
|
||||
if visible == 0 {
|
||||
return false
|
||||
}
|
||||
root, _, _ := procGetAncestor.Call(uintptr(hwnd), gaRoot)
|
||||
if root != uintptr(hwnd) {
|
||||
return false
|
||||
}
|
||||
owner, _, _ := procGetWindow.Call(uintptr(hwnd), gwOwner)
|
||||
if owner != 0 {
|
||||
return false
|
||||
}
|
||||
style, _, _ := procGetWindowLongPtr.Call(uintptr(hwnd), ^uintptr(15))
|
||||
if style&wsChild != 0 {
|
||||
return false
|
||||
}
|
||||
exe := executableForWindow(hwnd)
|
||||
e.mu.RLock()
|
||||
excluded := e.cfg.IsExcluded(exe)
|
||||
e.mu.RUnlock()
|
||||
return !excluded
|
||||
}
|
||||
|
||||
func executableForWindow(hwnd windows.Handle) string {
|
||||
var pid uint32
|
||||
procGetWindowThreadProcessID.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&pid)))
|
||||
if pid == 0 {
|
||||
return ""
|
||||
}
|
||||
h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer windows.CloseHandle(h)
|
||||
buf := make([]uint16, 32768)
|
||||
n := uint32(len(buf))
|
||||
if err := windows.QueryFullProcessImageName(h, 0, &buf[0], &n); err != nil {
|
||||
return ""
|
||||
}
|
||||
return windows.UTF16ToString(buf[:n])
|
||||
}
|
||||
|
||||
func monitorDetails(mon windows.Handle) (monitorInfoEx, bool) {
|
||||
mi := monitorInfoEx{Size: uint32(unsafe.Sizeof(monitorInfoEx{}))}
|
||||
ok, _, _ := procGetMonitorInfo.Call(uintptr(mon), uintptr(unsafe.Pointer(&mi)))
|
||||
return mi, ok != 0
|
||||
}
|
||||
func logMonitors() {
|
||||
callback := windows.NewCallback(func(mon, _, _, _ uintptr) uintptr {
|
||||
if mi, ok := monitorDetails(windows.Handle(mon)); ok {
|
||||
log.Printf("monitor %s work-area=(%d,%d)-(%d,%d)", mi.device(), mi.Work.Left, mi.Work.Top, mi.Work.Right, mi.Work.Bottom)
|
||||
}
|
||||
return 1
|
||||
})
|
||||
procEnumDisplayMonitors.Call(0, 0, callback, 0)
|
||||
}
|
||||
func (m monitorInfoEx) device() string { return windows.UTF16ToString(m.Device[:]) }
|
||||
func toLayoutRect(r rect) layout.Rect {
|
||||
return layout.Rect{Left: r.Left, Top: r.Top, Right: r.Right, Bottom: r.Bottom}
|
||||
}
|
||||
func keyDown(vk uintptr) bool { r, _, _ := procGetAsyncKeyState.Call(vk); return r&0x8000 != 0 }
|
||||
|
||||
func (e *engine) registerHotkeys() error {
|
||||
for i, h := range e.cfg.Hotkeys {
|
||||
mods, vk, err := parseHotkey(h.Keys)
|
||||
if err != nil {
|
||||
e.unregisterHotkeys()
|
||||
return fmt.Errorf("hotkey %q: %w", h.Keys, err)
|
||||
}
|
||||
id := i + 1
|
||||
ok, _, callErr := procRegisterHotKey.Call(0, uintptr(id), uintptr(mods|modNoRepeat), uintptr(vk))
|
||||
if ok == 0 {
|
||||
e.unregisterHotkeys()
|
||||
return fmt.Errorf("register hotkey %q (already in use?): %v", h.Keys, callErr)
|
||||
}
|
||||
e.hotkeys[id] = hotkeyBinding{id: id, action: h.Action, keys: h.Keys}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (e *engine) unregisterHotkeys() {
|
||||
for id := range e.hotkeys {
|
||||
procUnregisterHotKey.Call(0, uintptr(id))
|
||||
delete(e.hotkeys, id)
|
||||
}
|
||||
}
|
||||
|
||||
func parseHotkey(s string) (uint32, uint32, error) {
|
||||
parts := strings.Split(strings.ToLower(strings.ReplaceAll(s, " ", "")), "+")
|
||||
var mods, vk uint32
|
||||
regularKeys := 0
|
||||
for _, p := range parts {
|
||||
switch p {
|
||||
case "win", "windows":
|
||||
mods |= modWin
|
||||
case "alt":
|
||||
mods |= modAlt
|
||||
case "ctrl", "control":
|
||||
mods |= modControl
|
||||
case "shift":
|
||||
mods |= modShift
|
||||
case "left":
|
||||
vk = 0x25
|
||||
regularKeys++
|
||||
case "up":
|
||||
vk = 0x26
|
||||
regularKeys++
|
||||
case "right":
|
||||
vk = 0x27
|
||||
regularKeys++
|
||||
case "down":
|
||||
vk = 0x28
|
||||
regularKeys++
|
||||
case "pageup", "pgup":
|
||||
vk = 0x21
|
||||
regularKeys++
|
||||
case "pagedown", "pgdn":
|
||||
vk = 0x22
|
||||
regularKeys++
|
||||
default:
|
||||
if len(p) == 1 && ((p[0] >= 'a' && p[0] <= 'z') || (p[0] >= '0' && p[0] <= '9')) {
|
||||
vk = uint32(strings.ToUpper(p)[0])
|
||||
regularKeys++
|
||||
} else {
|
||||
return 0, 0, fmt.Errorf("unknown key %q", p)
|
||||
}
|
||||
}
|
||||
}
|
||||
if mods == 0 || vk == 0 || regularKeys != 1 {
|
||||
return 0, 0, fmt.Errorf("must contain modifiers and exactly one regular key")
|
||||
}
|
||||
return mods, vk, nil
|
||||
}
|
||||
|
||||
func (e *engine) rememberConfigTime() {
|
||||
if s, err := os.Stat(e.configPath); err == nil {
|
||||
e.configTime = s.ModTime()
|
||||
}
|
||||
}
|
||||
func (e *engine) reloadIfChanged() {
|
||||
s, err := os.Stat(e.configPath)
|
||||
if err != nil || !s.ModTime().After(e.configTime) {
|
||||
return
|
||||
}
|
||||
e.configTime = s.ModTime()
|
||||
cfg, err := config.Load(e.configPath)
|
||||
if err != nil {
|
||||
log.Printf("configuration reload rejected: %v", err)
|
||||
return
|
||||
}
|
||||
e.unregisterHotkeys()
|
||||
e.overlay.hide()
|
||||
e.overlayFailed = false
|
||||
e.mu.Lock()
|
||||
old := e.cfg
|
||||
e.cfg = cfg
|
||||
e.mu.Unlock()
|
||||
if err := e.registerHotkeys(); err != nil {
|
||||
log.Printf("configuration hotkeys rejected: %v; restoring previous configuration", err)
|
||||
e.mu.Lock()
|
||||
e.cfg = old
|
||||
e.mu.Unlock()
|
||||
_ = e.registerHotkeys()
|
||||
return
|
||||
}
|
||||
log.Printf("configuration reloaded")
|
||||
}
|
||||
Reference in New Issue
Block a user