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 } 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)}) } } // Park no centro da tela (cursor oculto, só para manter delta consistente) w, h := e.inputHandler.GetScreenResolution() cx, cy := w/2, h/2 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 janela em foco e traz KVMote pra frente, se necessário kvHwnd := e.inputHandler.GetAppHwnd() fgHwnd := e.inputHandler.GetForegroundWindow() if fgHwnd != kvHwnd { e.prevForegroundHwnd = fgHwnd e.prevWasActive = true if e.inputHandler.IsMinimized(kvHwnd) { e.inputHandler.RestoreWindow(kvHwnd) } 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 centro — scroll via Raw Input, cursor pode ficar visível w, h := e.inputHandler.GetScreenResolution() cx, cy := w/2, h/2 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 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 }