KVMote.go/internal/kvm/engine.go

485 lines
13 KiB
Go

package kvm
import (
"context"
"fmt"
"os"
"sync"
"time"
"github.com/atotto/clipboard"
"kvmote/internal/input"
"kvmote/internal/transport"
)
func LogDebug(msg string) {
f, err := os.OpenFile("kvmote_debug.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return
}
defer f.Close()
timestamp := time.Now().Format("15:04:05.000")
f.WriteString(fmt.Sprintf("[%s] %s\n", timestamp, msg))
}
type ClientPos int
const (
PosNone ClientPos = iota
PosLeft
PosRight
PosAbove
PosBelow
)
type ClientLayout int
const (
LayoutUS ClientLayout = iota
LayoutAbnt2
LayoutUsIntl
)
type WindowManager interface {
SetMiniMode()
RestoreNormalMode()
}
type Engine struct {
mu sync.Mutex
rawScrollOnce sync.Once
transport transport.Transport
inputHandler input.InputHandler
winManager WindowManager
clientMode bool
clientPos ClientPos
clientLayout ClientLayout
ctrlHeld bool
shiftHeld bool
altHeld bool
clipboardReady bool
virtualX, virtualY int32
pendingDX, pendingDY int32
lastRawPos input.Point
edgeEntry input.Point
isWarping bool
lastModeChange time.Time
scrollActive bool
scrollTimer time.Time
wheelAccum int32
mouseThrottle time.Time
prevForegroundHwnd uintptr // janela que estava em foco antes de entrar em client mode
prevWasActive bool // true se salvamos uma janela diferente do KVMote
kvmoteOrigX, kvmoteOrigY int32 // posição original da janela KVMote
kvmoteOrigW, kvmoteOrigH int32 // tamanho original da janela KVMote
kvmoteWasMinimized bool // se estava minimizado ao entrar em client mode
}
func NewEngine(t transport.Transport, h input.InputHandler) *Engine {
return &Engine{
transport: t,
inputHandler: h,
clientPos: PosRight,
}
}
func (e *Engine) SetWindowManager(wm WindowManager) {
e.winManager = wm
}
func (e *Engine) Transport() transport.Transport {
return e.transport
}
func (e *Engine) GetScreenResolution() (int32, int32) {
return e.inputHandler.GetScreenResolution()
}
func (e *Engine) GetWindowSize() (int32, int32) {
return e.inputHandler.GetWindowSize()
}
func (e *Engine) GetWindowPos() (int32, int32) {
return e.inputHandler.GetWindowPos()
}
func (e *Engine) GetMonitorWorkArea() (x, y, w, h int32) {
return e.inputHandler.GetMonitorWorkArea()
}
func (e *Engine) MoveWindow(x, y, w, h int32, topmost bool) {
e.inputHandler.MoveWindow(x, y, w, h, topmost)
}
func (e *Engine) GetDpiScale() float64 {
return e.inputHandler.GetDpiScale()
}
func (e *Engine) Start(ctx context.Context) error {
w, h := e.inputHandler.GetScreenResolution()
LogDebug(fmt.Sprintf("Engine Iniciada. Tela: %dx%d. Pos: %v", w, h, e.clientPos))
// Registra apenas uma vez — Connect() pode chamar Start() múltiplas vezes
e.rawScrollOnce.Do(func() {
if err := e.inputHandler.RegisterRawScrollSink(e.onRawScroll); err != nil {
LogDebug(fmt.Sprintf("Raw scroll sink falhou: %v", err))
} else {
LogDebug("Raw scroll sink registrado OK")
}
})
return e.inputHandler.Install(ctx, e.onMouse, e.onKey)
}
func (e *Engine) onRawScroll(delta int16) {
e.mu.Lock()
defer e.mu.Unlock()
if !e.clientMode || !e.transport.IsConnected() {
return
}
e.scrollActive = true
e.scrollTimer = time.Now()
// Mesmo acumulador e divisor do HandleManualScroll
// delta vem em unidades WHEEL_DELTA (±120 por notch)
e.wheelAccum += int32(delta)
const Divisor = 30
toSend := e.wheelAccum / Divisor
if toSend != 0 {
e.wheelAccum -= toSend * Divisor
val := int8(clamp(int(toSend), -127, 127))
go func(v int8) {
e.transport.Send([]byte{'W', byte(v)})
LogDebug(fmt.Sprintf("RAW SCROLL: delta=%d → W=%d", delta, v))
}(val)
}
}
func (e *Engine) onMouse(ev input.MouseEvent) bool {
e.mu.Lock()
defer e.mu.Unlock()
if !e.transport.IsConnected() {
return false
}
// LOG PARA DIAGNÓSTICO: Registrar qualquer mensagem que não seja movimento simples (0x0200)
if ev.Message != 0x0200 {
go LogDebug(fmt.Sprintf("MSG MOUSE: 0x%X | ClientMode: %v", ev.Message, e.clientMode))
}
if !e.clientMode {
if ev.Message == 0x0200 && e.isAtExitEdge(ev.Point) {
e.enterClientMode(ev.Point)
return true
}
return false
}
// ─── MODO CLIENTE ATIVO ───
switch ev.Message {
case 0x020A, 0x020E: // Roda Vertical ou Horizontal
// Não bloquear: evento passa para o webview → JS wheel → HandleManualScroll
// (WH_MOUSE_LL não recebe scroll de touchpad precision; mini janela captura via JS)
e.scrollActive = true
e.scrollTimer = time.Now()
return false
case 0x0200: // Move
if e.isWarping { e.isWarping = false; return true }
if e.scrollActive {
if time.Since(e.scrollTimer) > 250*time.Millisecond {
e.scrollActive = false
e.virtualX, e.virtualY = 0, 0
}
} else {
dx, dy := ev.Point.X-e.lastRawPos.X, ev.Point.Y-e.lastRawPos.Y
e.virtualX += dx
e.virtualY += dy
e.pendingDX += dx
e.pendingDY += dy
if e.shouldReturnToHost() {
if time.Since(e.lastModeChange) > 800*time.Millisecond {
e.exitClientMode()
return true
}
e.virtualX, e.virtualY = 0, 0
} else if time.Since(e.mouseThrottle) >= 40*time.Millisecond {
e.mouseThrottle = time.Now()
sdx, sdy := int8(clamp(int(e.pendingDX), -127, 127)), int8(clamp(int(e.pendingDY), -127, 127))
e.pendingDX, e.pendingDY = 0, 0
e.transport.SendLossy([]byte{'M', byte(sdx), byte(sdy)})
}
}
// Re-parka cursor no canto inferior direito (perto do relógio/bateria)
w, h := e.inputHandler.GetScreenResolution()
cx, cy := w-150, h-160
e.isWarping = true
e.inputHandler.SetCursorPos(cx, cy)
e.lastRawPos = input.Point{X: cx, Y: cy}
return true
case 0x0201: e.transport.Send([]byte{'D', 'L'}); return true
case 0x0202: e.transport.Send([]byte{'E', 'L'}); return true
case 0x0204: e.transport.Send([]byte{'D', 'R'}); return true
case 0x0205: e.transport.Send([]byte{'E', 'R'}); return true
}
return true
}
func (e *Engine) isAtExitEdge(p input.Point) bool {
w, h := e.inputHandler.GetScreenResolution()
const Margin = 10
switch e.clientPos {
case PosLeft: return p.X <= 0
case PosRight: return p.X >= w-Margin
case PosAbove: return p.Y <= 0
case PosBelow: return p.Y >= h-Margin
}
return false
}
func (e *Engine) shouldReturnToHost() bool {
switch e.clientPos {
case PosLeft: return e.virtualX > 600
case PosRight: return e.virtualX < -500
case PosBelow: return e.virtualY < -150
case PosAbove: return e.virtualY > 150
}
return false
}
func (e *Engine) enterClientMode(p input.Point) {
LogDebug(fmt.Sprintf("Entrando Modo Cliente em (%d, %d)", p.X, p.Y))
e.clientMode = true
e.edgeEntry = p
e.lastModeChange = time.Now()
e.virtualX, e.virtualY = 0, 0
e.pendingDX, e.pendingDY = 0, 0
e.wheelAccum = 0
e.mouseThrottle = time.Now()
// Salva estado original do KVMote (posição/tamanho/minimizado) e move pra direita
kvHwnd := e.inputHandler.GetAppHwnd()
e.kvmoteWasMinimized = e.inputHandler.IsMinimized(kvHwnd)
e.kvmoteOrigX, e.kvmoteOrigY, e.kvmoteOrigW, e.kvmoteOrigH = e.inputHandler.GetRestoredRect()
if e.kvmoteWasMinimized {
e.inputHandler.RestoreWindow(kvHwnd)
}
sw, sh := e.inputHandler.GetScreenResolution()
e.inputHandler.MoveWindow(sw-e.kvmoteOrigW, sh-350, e.kvmoteOrigW, 280, false)
LogDebug(fmt.Sprintf("KVMote movido para direita: x=%d y=%d w=%d h=300", sw-e.kvmoteOrigW, sh-350, e.kvmoteOrigW))
// Salva janela em foco e traz KVMote pra frente, se necessário
fgHwnd := e.inputHandler.GetForegroundWindow()
if fgHwnd != kvHwnd {
e.prevForegroundHwnd = fgHwnd
e.prevWasActive = true
e.inputHandler.RequestFocus()
LogDebug(fmt.Sprintf("Janela anterior salva: 0x%X, KVMote trazido ao foco", fgHwnd))
} else {
e.prevWasActive = false
e.prevForegroundHwnd = 0
}
// Mantém processo ativo + mostra overlay
if e.winManager != nil {
e.winManager.SetMiniMode()
}
// Parka cursor no canto inferior direito (perto do relógio/bateria)
w, h := e.inputHandler.GetScreenResolution()
cx, cy := w-150, h-160
e.isWarping = true
e.inputHandler.SetCursorPos(cx, cy)
e.lastRawPos = input.Point{X: cx, Y: cy}
e.transport.Send([]byte{'A'})
e.transport.Send([]byte{'O'})
}
func (e *Engine) exitClientMode() {
LogDebug("Saindo Modo Cliente.")
e.clientMode = false
e.lastModeChange = time.Now()
if e.winManager != nil {
e.winManager.RestoreNormalMode()
}
e.inputHandler.ShowCursor(true)
// Restaura posição/tamanho original do KVMote
if e.kvmoteOrigW > 0 {
e.inputHandler.MoveWindow(e.kvmoteOrigX, e.kvmoteOrigY, e.kvmoteOrigW, e.kvmoteOrigH, false)
LogDebug(fmt.Sprintf("KVMote restaurado: x=%d y=%d w=%d h=%d", e.kvmoteOrigX, e.kvmoteOrigY, e.kvmoteOrigW, e.kvmoteOrigH))
}
if e.kvmoteWasMinimized {
e.inputHandler.MinimizeWindow(e.inputHandler.GetAppHwnd())
e.kvmoteWasMinimized = false
}
// Restaura janela anterior ao foco
if e.prevWasActive && e.prevForegroundHwnd != 0 {
e.inputHandler.SetForegroundTo(e.prevForegroundHwnd)
LogDebug(fmt.Sprintf("Janela anterior restaurada: 0x%X", e.prevForegroundHwnd))
e.prevForegroundHwnd = 0
e.prevWasActive = false
}
w, h := e.inputHandler.GetScreenResolution()
var ret input.Point
const Offset = 120
// Retornamos o cursor exatamente para a borda onde ele entrou
switch e.clientPos {
case PosRight: ret = input.Point{X: w - Offset, Y: e.edgeEntry.Y}
case PosLeft: ret = input.Point{X: Offset, Y: e.edgeEntry.Y}
case PosAbove: ret = input.Point{X: e.edgeEntry.X, Y: Offset}
case PosBelow: ret = input.Point{X: e.edgeEntry.X, Y: h - Offset}
default: ret = input.Point{X: w / 2, Y: h / 2}
}
e.inputHandler.SetCursorPos(ret.X, ret.Y)
e.transport.Send([]byte{'H'})
e.transport.Send([]byte{'A'})
}
func (e *Engine) HandleManualScroll(delta int) {
e.mu.Lock()
defer e.mu.Unlock()
if !e.clientMode || !e.transport.IsConnected() {
return
}
e.scrollActive = true
e.scrollTimer = time.Now()
// Acumulamos o delta da UI para não perder movimentos pequenos
e.wheelAccum += int32(-delta) // Invertemos o delta da UI para bater com o padrão HID
// Divisor menor = Mais sensível
const Divisor = 15
toSend := e.wheelAccum / Divisor
if toSend != 0 {
e.wheelAccum -= toSend * Divisor
val := int8(clamp(int(toSend), -127, 127))
go func(v int8) {
e.transport.Send([]byte{'W', byte(v)})
LogDebug(fmt.Sprintf("UI SCROLL -> WHEEL %d (accum remain=%d)", v, e.wheelAccum))
}(val)
}
}
func (e *Engine) onKey(ev input.KeyboardEvent) bool {
e.mu.Lock()
defer e.mu.Unlock()
if !e.transport.IsConnected() { return false }
isDown := ev.Message == 0x0100 || ev.Message == 0x0104
switch ev.VKCode {
case 0xA2, 0xA3, 0x11: e.ctrlHeld = isDown
case 0xA0, 0xA1, 0x10: e.shiftHeld = isDown
case 0xA4, 0xA5, 0x12: e.altHeld = isDown
}
if !e.clientMode {
if isDown && ev.VKCode == 0x43 && e.ctrlHeld { e.clipboardReady = true }
return false
}
if isDown && ev.VKCode == 0x56 && e.ctrlHeld && e.clipboardReady {
e.clipboardReady = false
go e.sendClipboard()
return true
}
code, ok := vkToArduino(ev.VKCode)
if ok {
cmd := byte('U'); if isDown { cmd = 'P' }
e.transport.Send([]byte{cmd, code})
}
return true
}
func (e *Engine) sendClipboard() {
text, _ := clipboard.ReadAll()
if text == "" { return }
if len(text) > 2000 { text = text[:2000] }
data := []byte(text)
l := len(data)
e.transport.Send(append([]byte{'T', byte(l >> 8), byte(l & 0xFF)}, data...))
}
func clamp(v, min, max int) int {
if v < min { return min }; if v > max { return max }; return v
}
func (e *Engine) SendCtrlAltDel() {
LogDebug("Enviando CTRL+ALT+DEL...")
if !e.transport.IsConnected() {
LogDebug("Erro: Transporte não conectado.")
return
}
go func() {
e.transport.Send([]byte{'P', 0x80})
time.Sleep(10 * time.Millisecond)
e.transport.Send([]byte{'P', 0x82})
time.Sleep(10 * time.Millisecond)
e.transport.Send([]byte{'P', 0xD4})
time.Sleep(100 * time.Millisecond)
e.transport.Send([]byte{'U', 0xD4})
time.Sleep(10 * time.Millisecond)
e.transport.Send([]byte{'U', 0x82})
time.Sleep(10 * time.Millisecond)
e.transport.Send([]byte{'U', 0x80})
LogDebug("Sequência CTRL+ALT+DEL enviada.")
}()
}
func (e *Engine) SetPosition(pos int) {
e.mu.Lock()
defer e.mu.Unlock()
e.clientPos = ClientPos(pos)
}
func (e *Engine) SetLayout(layout int) {
e.mu.Lock()
defer e.mu.Unlock()
e.clientLayout = ClientLayout(layout)
}
var keyMap = map[uint32]byte{
0xA0: 0x81, 0xA1: 0x85, 0xA2: 0x80, 0xA3: 0x84, 0xA4: 0x82, 0xA5: 0x86, 0x5B: 0x83, 0x5C: 0x87,
0x10: 0x81, 0x11: 0x80, 0x12: 0x82, 0x70: 0xC2, 0x71: 0xC3, 0x72: 0xC4, 0x73: 0xC5, 0x74: 0xC6,
0x75: 0xC7, 0x76: 0xC8, 0x77: 0xC9, 0x78: 0xCA, 0x79: 0xCB, 0x7A: 0xCC, 0x7B: 0xCD, 0x26: 0xDA,
0x28: 0xD9, 0x25: 0xD8, 0x27: 0xD7, 0x24: 0xD2, 0x23: 0xD5, 0x21: 0xD3, 0x22: 0xD6, 0x2D: 0xD1,
0x2E: 0xD4, 0x0D: 0xB0, 0x1B: 0xB1, 0x08: 0xB2, 0x09: 0xB3, 0x14: 0xC1, 0x2C: 0xCE, 0x91: 0xCF, 0x13: 0xD0,
}
func vkToArduino(vk uint32) (byte, bool) {
if m, ok := keyMap[vk]; ok { return m, true }
if vk >= 0x41 && vk <= 0x5A { return byte(vk + 0x20), true }
if vk >= 0x30 && vk <= 0x39 { return byte(vk), true }
if vk >= 0x60 && vk <= 0x69 { return byte('0' + vk - 0x60), true }
switch vk {
case 0x20: return ' ', true; case 0xBD: return '-', true; case 0xBB: return '=', true
case 0xDB: return '[', true; case 0xDD: return ']', true; case 0xDC: return '\\', true
case 0xBA: return ';', true; case 0xDE: return '\'', true; case 0xBC: return ',', true
case 0xBE: return '.', true; case 0xBF: return '/', true; case 0xC0: return '`', true
case 0xE2: return 0xEC, true
}
return 0, false
}