485 lines
13 KiB
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 < -300
|
|
case PosAbove: return e.virtualY > 300
|
|
}
|
|
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
|
|
}
|