Initial commit with .gitignore

This commit is contained in:
Ricardo Carneiro 2026-04-24 10:10:17 -03:00
commit b19e63f847
26 changed files with 2520 additions and 0 deletions

23
.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with Litmus
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
# Wails
frontend/dist/
build/bin/
kvmote_debug.log

17
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,17 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Wails: Debug Backend (Go)",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceRoot}",
"buildFlags": "-tags desktop,debug",
"args": [],
"env": {
"WAILS_DEBUG": "true"
}
}
]
}

4
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"go.buildTags": "desktop,debug",
"go.lintFlags": ["--tags", "desktop,debug"]
}

128
CLAUDE.md Normal file
View File

@ -0,0 +1,128 @@
# CLAUDE.md — KVMote (Go/Wails)
Reescrita em Go do KVMote (original em C#/WinForms em `C:\vscode\KVMote`).
---
## O que é
KVM over Bluetooth/BLE. Controla PC remoto (cliente) a partir do host usando microcontrolador como HID USB. **Sem software no PC cliente.**
```
Host PC ──BLE NUS──► ESP32-S3 ──USB HID──► Cliente PC (sem sw)
```
Hardware suportado: ESP32-S3 (BLE NUS) e Arduino Leonardo + HC-06 (Serial SPP). Ver CLAUDE.md do projeto C# para detalhes de hardware e firmware.
---
## Stack
- **Go 1.23** + **Wails v2** (webview desktop)
- **tinygo.org/x/bluetooth** — BLE via WinRT
- **golang.org/x/sys/windows** — hooks globais (WH_MOUSE_LL, WH_KEYBOARD_LL)
- **github.com/atotto/clipboard** — leitura de clipboard
- Frontend: HTML estático em `frontend/dist/` (sem framework JS)
---
## Estrutura
```
main.go — entry point Wails, bind App
app.go — App struct, métodos expostos ao frontend
internal/
transport/
transport.go — interface Transport (Detect/Connect/Send/SendLossy...)
ble_windows.go — BLE NUS via tinygo/bluetooth
kvm/
engine.go — lógica KVM: mouse, teclado, clipboard, modo cliente
input/
input.go — interface InputHandler + tipos (Point, MouseEvent, KeyboardEvent)
input_windows.go — hooks Win32, SetCursorPos, ShowCursor, GetSystemMetrics
frontend/
dist/index.html — UI
wails.json — config Wails
```
---
## Protocolo binário (Host → Dispositivo)
Idêntico ao projeto C#:
| Cmd | Bytes | Ação |
|-----|-------|------|
| `M` dx dy | 3 | Mouse move (int8) |
| `W` delta | 2 | Scroll (int8) |
| `K` char | 2 | Keyboard.write |
| `P` key | 2 | Keyboard.press |
| `U` key | 2 | Keyboard.release |
| `A` | 1 | releaseAll |
| `C` L\|R | 2 | Click |
| `D` L\|R | 2 | Mouse press |
| `E` L\|R | 2 | Mouse release |
| `T` lenH lenL data | 3+N | Clipboard batch (Go-specific) |
| `O` | 1 | LED magenta (modo cliente) |
| `H` | 1 | LED azul (host conectado) |
| `G` | 1 | LED verde (desconectado) |
| `~` | 1 | Ping → `[PONG]` |
---
## Lógica KVM (engine.go)
- **Entrada modo cliente:** cursor atinge borda configurada → esconde cursor, warp centro, acumula deltas (técnica FPS)
- **Retorno ao host:** coordenadas virtuais cruzam `-ReturnThreshold` (120px) na direção de entrada
- **Debounce:** 800ms anti-bounce após troca de modo
- **Mouse throttle:** 40ms (~25 pacotes/s)
- **Scroll:** `scrollActive` suprime warp por 200ms durante scroll (evita cancelar gesto touchpad)
- **Clipboard:** Ctrl+C no host seta `clipboardReady`, Ctrl+V em modo cliente envia via comando `T` (batch)
- **Ctrl+Alt+Del:** sequência P/U com delay 50ms
---
## Hooks Win32 (input_windows.go)
- `runtime.LockOSThread()` obrigatório — hooks Win32 exigem message pump na mesma thread
- Loop `GetMessageW` mantém thread viva
- `Uninstall()` via `PostThreadMessageW(WM_QUIT)`
- `SetProcessDPIAware` chamado no init()
---
## Build
```bash
# Dev
wails dev
# Produção
wails build
```
Saída: `build/bin/kvmote.exe`
---
## Diferenças do projeto C#
| Aspecto | C# (WinForms) | Go (Wails) |
|---------|---------------|------------|
| UI | WinForms nativo | Webview (HTML) |
| Transporte | Serial + BLE (dual) | Só BLE (por enquanto) |
| Clipboard | char-a-char com delay | Batch via comando `T` |
| ClipboardMax | 500/1000 chars | 65536 chars |
| Scroll | acumulador _wheelAccum + _scrollActive | scrollActive only |
| ReturnThreshold | 15px | 120px |
| Reconexão | auto-reconnect loop | manual |
| Heartbeat/Watchdog | sim | não implementado |
---
## ⚠️ Cuidados
- **Scroll touchpad:** `scrollActive` e `scrollTimer` não devem ser resetados em enter/exitClientMode
- **runtime.LockOSThread:** nunca remover da goroutine de hooks
- **isWarping flag:** previne loop infinito SetCursorPos → WM_MOUSEMOVE → SetCursorPos
- **Mutex ordering:** engine.mu protege todo estado KVM; transport.mu protege conexão BLE

78
app.go Normal file
View File

@ -0,0 +1,78 @@
package main
import (
"context"
"fmt"
"time"
"kvmote/internal/input"
"kvmote/internal/kvm"
"kvmote/internal/transport"
)
// App struct
type App struct {
ctx context.Context
engine *kvm.Engine
}
// NewApp creates a new App application struct
func NewApp() *App {
t := transport.NewBleTransport()
h := input.NewInputHandler()
e := kvm.NewEngine(t, h)
return &App{
engine: e,
}
}
// startup is called when the app starts. The context is saved
// so we can call the runtime methods
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
err := a.engine.Start(ctx)
if err != nil {
fmt.Printf("Error starting engine: %v\n", err)
}
}
func (a *App) Connect() string {
err := a.engine.Start(a.ctx)
if err != nil {
return fmt.Sprintf("Erro Hook: %v", err)
}
// Lógica real de conexão Bluetooth
ctx, cancel := context.WithTimeout(a.ctx, 10*time.Second)
defer cancel()
ok, err := a.engine.Transport().Detect(ctx)
if err != nil || !ok {
return "Erro: KVMote não encontrado"
}
err = a.engine.Transport().Connect(ctx)
if err != nil {
return fmt.Sprintf("Erro Conexão: %v", err)
}
return "Conectado"
}
func (a *App) Disconnect() string {
a.engine.Transport().Disconnect()
return "Desconectado"
}
func (a *App) SendCtrlAltDel() {
kvm.LogDebug("App: Chamando SendCtrlAltDel")
a.engine.SendCtrlAltDel()
}
func (a *App) ChangeLayout(layout int) {
a.engine.SetLayout(layout)
}
func (a *App) SetPosition(pos int) {
a.engine.SetPosition(pos)
}

BIN
build/appicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

BIN
build/windows/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

15
build/windows/info.json Normal file
View File

@ -0,0 +1,15 @@
{
"fixed": {
"file_version": "{{.Info.ProductVersion}}"
},
"info": {
"0000": {
"ProductVersion": "{{.Info.ProductVersion}}",
"CompanyName": "{{.Info.CompanyName}}",
"FileDescription": "{{.Info.ProductName}}",
"LegalCopyright": "{{.Info.Copyright}}",
"ProductName": "{{.Info.ProductName}}",
"Comments": "{{.Info.Comments}}"
}
}
}

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity type="win32" name="com.wails.{{.Name}}" version="{{.Info.ProductVersion}}.0" processorArchitecture="*"/>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
</dependentAssembly>
</dependency>
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
</asmv3:windowsSettings>
</asmv3:application>
</assembly>

226
frontend/dist/index.html vendored Normal file
View File

@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KVMote</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<style>
:root {
--bg-color: #121212;
--card-bg: #1e1e1e;
--text-color: #e0e0e0;
--accent-color: #3273dc;
--border-color: #333;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
height: 100vh;
margin: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
overflow: hidden;
}
.app-container {
padding: 20px;
max-width: 400px;
margin: auto;
}
.section-title {
font-size: 0.9rem;
color: #888;
margin-bottom: 15px;
text-align: center;
}
/* Monitor Cross Layout */
.monitor-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
gap: 10px;
width: 240px;
margin: 0 auto 30px auto;
}
.monitor-btn {
background-color: #333;
border: 1px solid #444;
color: #bbb;
padding: 10px 5px;
cursor: pointer;
font-size: 0.8rem;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.monitor-btn:hover { background-color: #444; }
.monitor-btn.active {
background-color: #2c3e50;
border-color: #3498db;
color: white;
}
.host-pc {
grid-column: 2;
grid-row: 2;
background-color: #1a2a3a;
border: 1px solid var(--accent-color);
color: #3498db;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
}
.field-label { font-size: 0.85rem; color: #aaa; margin-bottom: 5px; }
.control-panel { background: var(--card-bg); padding: 15px; border-radius: 4px; border: 1px solid var(--border-color); }
.status-bar {
position: fixed;
bottom: 0;
width: 100%;
background: #1a1a1a;
padding: 5px 15px;
font-size: 0.75rem;
color: #666;
border-top: 1px solid #222;
}
.status-dot { height: 8px; width: 8px; border-radius: 50%; display: inline-block; margin-right: 8px; }
/* Custom UI components */
.custom-select {
width: 100%;
background: #252525;
color: white;
border: 1px solid #444;
padding: 5px;
font-size: 0.9rem;
}
.btn-action {
background: #333;
border: 1px solid #444;
color: white;
padding: 8px;
width: 100%;
cursor: pointer;
margin-bottom: 10px;
font-size: 0.9rem;
}
.btn-action:hover { background: #444; }
.btn-connect {
background: var(--accent-color);
border: none;
color: white;
font-weight: bold;
}
.btn-cad { margin-top: 10px; background: #2a2a2a; color: #ccc; }
</style>
</head>
<body x-data="kvmApp()">
<div class="app-container">
<div class="section-title">Posição do PC Cliente:</div>
<div class="monitor-grid">
<!-- Top -->
<button class="monitor-btn" style="grid-column: 2; grid-row: 1;"
:class="pos === 'above' ? 'active' : ''" @click="setPos('above')">Acima</button>
<!-- Left -->
<button class="monitor-btn" style="grid-column: 1; grid-row: 2;"
:class="pos === 'left' ? 'active' : ''" @click="setPos('left')">Esquerda</button>
<!-- Center -->
<div class="host-pc">[HOST PC]</div>
<!-- Right -->
<button class="monitor-btn" style="grid-column: 3; grid-row: 2;"
:class="pos === 'right' ? 'active' : ''" @click="setPos('right')">Direita</button>
<!-- Bottom -->
<button class="monitor-btn" style="grid-column: 2; grid-row: 3;"
:class="pos === 'below' ? 'active' : ''" @click="setPos('below')">Abaixo</button>
</div>
<div class="is-flex is-align-items-center mb-4" style="font-size: 0.85rem; color: #888;">
<span class="status-dot" :style="{ backgroundColor: detected ? '#3498db' : '#555' }"></span>
<span x-text="detected ? 'KVMote (BLE) detectado' : 'Buscando dispositivo...'"></span>
</div>
<div class="columns is-mobile is-gapless mb-2">
<div class="column pr-1">
<button class="btn-action" @click="detect()">Detectar</button>
</div>
<div class="column pl-1">
<button class="btn-action btn-connect" @click="toggleConnect()" x-text="connected ? 'Desconectar' : 'Conectar'"></button>
</div>
</div>
<div class="field mb-4">
<div class="is-flex is-align-items-center is-justify-content-space-between">
<label class="field-label">Layout do cliente:</label>
<select class="custom-select" style="width: 60%;" x-model="layout" @change="setLayout()">
<option value="0">US / EN</option>
<option value="1">PT-BR ABNT2</option>
<option value="2">US International</option>
</select>
</div>
</div>
<button class="btn-action btn-cad" @click="sendCAD()">Enviar Ctrl+Alt+Del</button>
<div class="field mt-4">
<div class="is-flex is-align-items-center is-justify-content-space-between">
<label class="field-label">Ao fechar:</label>
<select class="custom-select" style="width: 60%;">
<option value="tray">Minimizar para Tray</option>
<option value="exit">Fechar aplicação</option>
</select>
</div>
</div>
</div>
<div class="status-bar">
<span x-text="statusText"></span>
</div>
<script>
function kvmApp() {
return {
connected: false,
detected: true,
statusText: 'Desconectado',
pos: 'right',
layout: 1,
toggleConnect() {
if (this.connected) {
window.go.main.App.Disconnect().then(res => {
this.connected = false;
this.statusText = res;
});
} else {
window.go.main.App.Connect().then(res => {
this.connected = true;
this.statusText = 'Conectado';
});
}
},
setPos(p) {
this.pos = p;
const map = { 'left': 1, 'right': 2, 'above': 3, 'below': 4 };
window.go.main.App.SetPosition(map[p]);
},
setLayout() {
window.go.main.App.ChangeLayout(parseInt(this.layout));
},
sendCAD() {
window.go.main.App.SendCtrlAltDel();
},
detect() {
// Chamar detecção no backend
}
}
}
</script>
</body>
</html>

BIN
frontend/dist/placeholder.txt vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,12 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function ChangeLayout(arg1:number):Promise<void>;
export function Connect():Promise<string>;
export function Disconnect():Promise<string>;
export function SendCtrlAltDel():Promise<void>;
export function SetPosition(arg1:number):Promise<void>;

View File

@ -0,0 +1,23 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function ChangeLayout(arg1) {
return window['go']['main']['App']['ChangeLayout'](arg1);
}
export function Connect() {
return window['go']['main']['App']['Connect']();
}
export function Disconnect() {
return window['go']['main']['App']['Disconnect']();
}
export function SendCtrlAltDel() {
return window['go']['main']['App']['SendCtrlAltDel']();
}
export function SetPosition(arg1) {
return window['go']['main']['App']['SetPosition'](arg1);
}

View File

@ -0,0 +1,24 @@
{
"name": "@wailsapp/runtime",
"version": "2.0.0",
"description": "Wails Javascript runtime library",
"main": "runtime.js",
"types": "runtime.d.ts",
"scripts": {
},
"repository": {
"type": "git",
"url": "git+https://github.com/wailsapp/wails.git"
},
"keywords": [
"Wails",
"Javascript",
"Go"
],
"author": "Lea Anthony <lea.anthony@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/wailsapp/wails/issues"
},
"homepage": "https://github.com/wailsapp/wails#readme"
}

View File

@ -0,0 +1,330 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The electron alternative for Go
(c) Lea Anthony 2019-present
*/
export interface Position {
x: number;
y: number;
}
export interface Size {
w: number;
h: number;
}
export interface Screen {
isCurrent: boolean;
isPrimary: boolean;
width : number
height : number
}
// Environment information such as platform, buildtype, ...
export interface EnvironmentInfo {
buildType: string;
platform: string;
arch: string;
}
// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
// emits the given event. Optional data may be passed with the event.
// This will trigger any event listeners.
export function EventsEmit(eventName: string, ...data: any): void;
// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
// sets up a listener for the given event name, but will only trigger a given number times.
export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
// sets up a listener for the given event name, but will only trigger once.
export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
// unregisters the listener for the given event name.
export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
// unregisters all listeners.
export function EventsOffAll(): void;
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
// logs the given message as a raw message
export function LogPrint(message: string): void;
// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
// logs the given message at the `trace` log level.
export function LogTrace(message: string): void;
// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
// logs the given message at the `debug` log level.
export function LogDebug(message: string): void;
// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
// logs the given message at the `error` log level.
export function LogError(message: string): void;
// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
// logs the given message at the `fatal` log level.
// The application will quit after calling this method.
export function LogFatal(message: string): void;
// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
// logs the given message at the `info` log level.
export function LogInfo(message: string): void;
// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
// logs the given message at the `warning` log level.
export function LogWarning(message: string): void;
// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
// Forces a reload by the main application as well as connected browsers.
export function WindowReload(): void;
// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
// Reloads the application frontend.
export function WindowReloadApp(): void;
// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
// Sets the window AlwaysOnTop or not on top.
export function WindowSetAlwaysOnTop(b: boolean): void;
// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
// *Windows only*
// Sets window theme to system default (dark/light).
export function WindowSetSystemDefaultTheme(): void;
// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
// *Windows only*
// Sets window to light theme.
export function WindowSetLightTheme(): void;
// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
// *Windows only*
// Sets window to dark theme.
export function WindowSetDarkTheme(): void;
// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
// Centers the window on the monitor the window is currently on.
export function WindowCenter(): void;
// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
// Sets the text in the window title bar.
export function WindowSetTitle(title: string): void;
// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
// Makes the window full screen.
export function WindowFullscreen(): void;
// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
// Restores the previous window dimensions and position prior to full screen.
export function WindowUnfullscreen(): void;
// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
// Returns the state of the window, i.e. whether the window is in full screen mode or not.
export function WindowIsFullscreen(): Promise<boolean>;
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
// Sets the width and height of the window.
export function WindowSetSize(width: number, height: number): void;
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
// Gets the width and height of the window.
export function WindowGetSize(): Promise<Size>;
// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
// Setting a size of 0,0 will disable this constraint.
export function WindowSetMaxSize(width: number, height: number): void;
// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
// Setting a size of 0,0 will disable this constraint.
export function WindowSetMinSize(width: number, height: number): void;
// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
// Sets the window position relative to the monitor the window is currently on.
export function WindowSetPosition(x: number, y: number): void;
// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
// Gets the window position relative to the monitor the window is currently on.
export function WindowGetPosition(): Promise<Position>;
// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
// Hides the window.
export function WindowHide(): void;
// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
// Shows the window, if it is currently hidden.
export function WindowShow(): void;
// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
// Maximises the window to fill the screen.
export function WindowMaximise(): void;
// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
// Toggles between Maximised and UnMaximised.
export function WindowToggleMaximise(): void;
// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
// Restores the window to the dimensions and position prior to maximising.
export function WindowUnmaximise(): void;
// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
// Returns the state of the window, i.e. whether the window is maximised or not.
export function WindowIsMaximised(): Promise<boolean>;
// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
// Minimises the window.
export function WindowMinimise(): void;
// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
// Restores the window to the dimensions and position prior to minimising.
export function WindowUnminimise(): void;
// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
// Returns the state of the window, i.e. whether the window is minimised or not.
export function WindowIsMinimised(): Promise<boolean>;
// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
// Returns the state of the window, i.e. whether the window is normal or not.
export function WindowIsNormal(): Promise<boolean>;
// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
export function ScreenGetAll(): Promise<Screen[]>;
// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
// Opens the given URL in the system browser.
export function BrowserOpenURL(url: string): void;
// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
// Returns information about the environment
export function Environment(): Promise<EnvironmentInfo>;
// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
// Quits the application.
export function Quit(): void;
// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
// Hides the application.
export function Hide(): void;
// [Show](https://wails.io/docs/reference/runtime/intro#show)
// Shows the application.
export function Show(): void;
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
// Returns the current text stored on clipboard
export function ClipboardGetText(): Promise<string>;
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
// Sets a text on the clipboard
export function ClipboardSetText(text: string): Promise<boolean>;
// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
// OnFileDropOff removes the drag and drop listeners and handlers.
export function OnFileDropOff() :void
// Check if the file path resolver is available
export function CanResolveFilePaths(): boolean;
// Resolves file paths for an array of files
export function ResolveFilePaths(files: File[]): void
// Notification types
export interface NotificationOptions {
id: string;
title: string;
subtitle?: string; // macOS and Linux only
body?: string;
categoryId?: string;
data?: { [key: string]: any };
}
export interface NotificationAction {
id?: string;
title?: string;
destructive?: boolean; // macOS-specific
}
export interface NotificationCategory {
id?: string;
actions?: NotificationAction[];
hasReplyField?: boolean;
replyPlaceholder?: string;
replyButtonTitle?: string;
}
// [InitializeNotifications](https://wails.io/docs/reference/runtime/notification#initializenotifications)
// Initializes the notification service for the application.
// This must be called before sending any notifications.
export function InitializeNotifications(): Promise<void>;
// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications)
// Cleans up notification resources and releases any held connections.
export function CleanupNotifications(): Promise<void>;
// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable)
// Checks if notifications are available on the current platform.
export function IsNotificationAvailable(): Promise<boolean>;
// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization)
// Requests notification authorization from the user (macOS only).
export function RequestNotificationAuthorization(): Promise<boolean>;
// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization)
// Checks the current notification authorization status (macOS only).
export function CheckNotificationAuthorization(): Promise<boolean>;
// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification)
// Sends a basic notification with the given options.
export function SendNotification(options: NotificationOptions): Promise<void>;
// [SendNotificationWithActions](https://wails.io/docs/reference/runtime/notification#sendnotificationwithactions)
// Sends a notification with action buttons. Requires a registered category.
export function SendNotificationWithActions(options: NotificationOptions): Promise<void>;
// [RegisterNotificationCategory](https://wails.io/docs/reference/runtime/notification#registernotificationcategory)
// Registers a notification category that can be used with SendNotificationWithActions.
export function RegisterNotificationCategory(category: NotificationCategory): Promise<void>;
// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory)
// Removes a previously registered notification category.
export function RemoveNotificationCategory(categoryId: string): Promise<void>;
// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications)
// Removes all pending notifications from the notification center.
export function RemoveAllPendingNotifications(): Promise<void>;
// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification)
// Removes a specific pending notification by its identifier.
export function RemovePendingNotification(identifier: string): Promise<void>;
// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications)
// Removes all delivered notifications from the notification center.
export function RemoveAllDeliveredNotifications(): Promise<void>;
// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification)
// Removes a specific delivered notification by its identifier.
export function RemoveDeliveredNotification(identifier: string): Promise<void>;
// [RemoveNotification](https://wails.io/docs/reference/runtime/notification#removenotification)
// Removes a notification by its identifier (cross-platform convenience function).
export function RemoveNotification(identifier: string): Promise<void>;

View File

@ -0,0 +1,298 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The electron alternative for Go
(c) Lea Anthony 2019-present
*/
export function LogPrint(message) {
window.runtime.LogPrint(message);
}
export function LogTrace(message) {
window.runtime.LogTrace(message);
}
export function LogDebug(message) {
window.runtime.LogDebug(message);
}
export function LogInfo(message) {
window.runtime.LogInfo(message);
}
export function LogWarning(message) {
window.runtime.LogWarning(message);
}
export function LogError(message) {
window.runtime.LogError(message);
}
export function LogFatal(message) {
window.runtime.LogFatal(message);
}
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
}
export function EventsOn(eventName, callback) {
return EventsOnMultiple(eventName, callback, -1);
}
export function EventsOff(eventName, ...additionalEventNames) {
return window.runtime.EventsOff(eventName, ...additionalEventNames);
}
export function EventsOffAll() {
return window.runtime.EventsOffAll();
}
export function EventsOnce(eventName, callback) {
return EventsOnMultiple(eventName, callback, 1);
}
export function EventsEmit(eventName) {
let args = [eventName].slice.call(arguments);
return window.runtime.EventsEmit.apply(null, args);
}
export function WindowReload() {
window.runtime.WindowReload();
}
export function WindowReloadApp() {
window.runtime.WindowReloadApp();
}
export function WindowSetAlwaysOnTop(b) {
window.runtime.WindowSetAlwaysOnTop(b);
}
export function WindowSetSystemDefaultTheme() {
window.runtime.WindowSetSystemDefaultTheme();
}
export function WindowSetLightTheme() {
window.runtime.WindowSetLightTheme();
}
export function WindowSetDarkTheme() {
window.runtime.WindowSetDarkTheme();
}
export function WindowCenter() {
window.runtime.WindowCenter();
}
export function WindowSetTitle(title) {
window.runtime.WindowSetTitle(title);
}
export function WindowFullscreen() {
window.runtime.WindowFullscreen();
}
export function WindowUnfullscreen() {
window.runtime.WindowUnfullscreen();
}
export function WindowIsFullscreen() {
return window.runtime.WindowIsFullscreen();
}
export function WindowGetSize() {
return window.runtime.WindowGetSize();
}
export function WindowSetSize(width, height) {
window.runtime.WindowSetSize(width, height);
}
export function WindowSetMaxSize(width, height) {
window.runtime.WindowSetMaxSize(width, height);
}
export function WindowSetMinSize(width, height) {
window.runtime.WindowSetMinSize(width, height);
}
export function WindowSetPosition(x, y) {
window.runtime.WindowSetPosition(x, y);
}
export function WindowGetPosition() {
return window.runtime.WindowGetPosition();
}
export function WindowHide() {
window.runtime.WindowHide();
}
export function WindowShow() {
window.runtime.WindowShow();
}
export function WindowMaximise() {
window.runtime.WindowMaximise();
}
export function WindowToggleMaximise() {
window.runtime.WindowToggleMaximise();
}
export function WindowUnmaximise() {
window.runtime.WindowUnmaximise();
}
export function WindowIsMaximised() {
return window.runtime.WindowIsMaximised();
}
export function WindowMinimise() {
window.runtime.WindowMinimise();
}
export function WindowUnminimise() {
window.runtime.WindowUnminimise();
}
export function WindowSetBackgroundColour(R, G, B, A) {
window.runtime.WindowSetBackgroundColour(R, G, B, A);
}
export function ScreenGetAll() {
return window.runtime.ScreenGetAll();
}
export function WindowIsMinimised() {
return window.runtime.WindowIsMinimised();
}
export function WindowIsNormal() {
return window.runtime.WindowIsNormal();
}
export function BrowserOpenURL(url) {
window.runtime.BrowserOpenURL(url);
}
export function Environment() {
return window.runtime.Environment();
}
export function Quit() {
window.runtime.Quit();
}
export function Hide() {
window.runtime.Hide();
}
export function Show() {
window.runtime.Show();
}
export function ClipboardGetText() {
return window.runtime.ClipboardGetText();
}
export function ClipboardSetText(text) {
return window.runtime.ClipboardSetText(text);
}
/**
* Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
*
* @export
* @callback OnFileDropCallback
* @param {number} x - x coordinate of the drop
* @param {number} y - y coordinate of the drop
* @param {string[]} paths - A list of file paths.
*/
/**
* OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
*
* @export
* @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
* @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
*/
export function OnFileDrop(callback, useDropTarget) {
return window.runtime.OnFileDrop(callback, useDropTarget);
}
/**
* OnFileDropOff removes the drag and drop listeners and handlers.
*/
export function OnFileDropOff() {
return window.runtime.OnFileDropOff();
}
export function CanResolveFilePaths() {
return window.runtime.CanResolveFilePaths();
}
export function ResolveFilePaths(files) {
return window.runtime.ResolveFilePaths(files);
}
export function InitializeNotifications() {
return window.runtime.InitializeNotifications();
}
export function CleanupNotifications() {
return window.runtime.CleanupNotifications();
}
export function IsNotificationAvailable() {
return window.runtime.IsNotificationAvailable();
}
export function RequestNotificationAuthorization() {
return window.runtime.RequestNotificationAuthorization();
}
export function CheckNotificationAuthorization() {
return window.runtime.CheckNotificationAuthorization();
}
export function SendNotification(options) {
return window.runtime.SendNotification(options);
}
export function SendNotificationWithActions(options) {
return window.runtime.SendNotificationWithActions(options);
}
export function RegisterNotificationCategory(category) {
return window.runtime.RegisterNotificationCategory(category);
}
export function RemoveNotificationCategory(categoryId) {
return window.runtime.RemoveNotificationCategory(categoryId);
}
export function RemoveAllPendingNotifications() {
return window.runtime.RemoveAllPendingNotifications();
}
export function RemovePendingNotification(identifier) {
return window.runtime.RemovePendingNotification(identifier);
}
export function RemoveAllDeliveredNotifications() {
return window.runtime.RemoveAllDeliveredNotifications();
}
export function RemoveDeliveredNotification(identifier) {
return window.runtime.RemoveDeliveredNotification(identifier);
}
export function RemoveNotification(identifier) {
return window.runtime.RemoveNotification(identifier);
}

48
go.mod Normal file
View File

@ -0,0 +1,48 @@
module kvmote
go 1.23.8
require (
github.com/atotto/clipboard v0.1.4
github.com/wailsapp/wails/v2 v2.12.0
golang.org/x/sys v0.30.0
tinygo.org/x/bluetooth v0.15.0
)
require (
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
github.com/bep/debounce v1.2.1 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/labstack/echo/v4 v4.13.3 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
github.com/leaanthony/gosod v1.0.4 // indirect
github.com/leaanthony/slicer v1.6.0 // indirect
github.com/leaanthony/u v1.1.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/saltosystems/winrt-go v0.0.0-20260317170058-9c2fec580d96 // indirect
github.com/samber/lo v1.49.1 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/soypat/cyw43439 v0.1.0 // indirect
github.com/soypat/lneto v0.1.0 // indirect
github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710 // indirect
github.com/tinygo-org/cbgo v0.0.4 // indirect
github.com/tinygo-org/pio v0.3.0 // indirect
github.com/tkrajina/go-reflector v0.5.8 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/wailsapp/go-webview2 v1.0.22 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/text v0.22.0 // indirect
)

113
go.sum Normal file
View File

@ -0,0 +1,113 @@
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA=
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/saltosystems/winrt-go v0.0.0-20260317170058-9c2fec580d96 h1:IXxzj3yjfDNXZJ35foY+RpFShqPsZZ81hhCckgfh5PI=
github.com/saltosystems/winrt-go v0.0.0-20260317170058-9c2fec580d96/go.mod h1:CIltaIm7qaANUIvzr0Vmz71lmQMAIbGJ7cvgzX7FMfA=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/soypat/cyw43439 v0.1.0 h1:3Nyqg2LSndhCYgCr2VXuL2nn73vyaJXAnD02veMoLvA=
github.com/soypat/cyw43439 v0.1.0/go.mod h1:R2uSILRwSPmcmmKy5Z0FtK4ypgiPf5YqK+F+IKmXqxc=
github.com/soypat/lneto v0.1.0 h1:VAHCJ33hvC3wDqhM0Vm7w0k6vwNsOCAsQ8XTrXJpS7I=
github.com/soypat/lneto v0.1.0/go.mod h1:g/8Lk+hIsMZydyWDJjK2YfsCuG6jA5mWCO6U+4S7w1U=
github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710 h1:Y9fBuiR/urFY/m76+SAZTxk2xAOS2n85f+H1CugajeA=
github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710/go.mod h1:oCVCNGCHMKoBj97Zp9znLbQ1nHxpkmOY9X+UAGzOxc8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tinygo-org/cbgo v0.0.4 h1:3D76CRYbH03Rudi8sEgs/YO0x3JIMdyq8jlQtk/44fU=
github.com/tinygo-org/cbgo v0.0.4/go.mod h1:7+HgWIHd4nbAz0ESjGlJ1/v9LDU1Ox8MGzP9mah/fLk=
github.com/tinygo-org/pio v0.3.0 h1:opEnOtw58KGB4RJD3/n/Rd0/djYGX3DeJiXLI6y/yDI=
github.com/tinygo-org/pio v0.3.0/go.mod h1:wf6c6lKZp+pQOzKKcpzchmRuhiMc27ABRuo7KVnaMFU=
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58=
github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/wails/v2 v2.12.0 h1:BHO/kLNWFHYjCzucxbzAYZWUjub1Tvb4cSguQozHn5c=
github.com/wailsapp/wails/v2 v2.12.0/go.mod h1:mo1bzK1DEJrobt7YrBjgxvb5Sihb1mhAY09hppbibQg=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0=
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
tinygo.org/x/bluetooth v0.15.0 h1:hLn8+iZFXvVxBzPIdZfvc6TD8JP32ixF22lCEWHAbIo=
tinygo.org/x/bluetooth v0.15.0/go.mod h1:meayNB+9rC1igTUNmNU7KftlSEzrFHe37rBSQZjHN8Y=

28
internal/input/input.go Normal file
View File

@ -0,0 +1,28 @@
package input
import "context"
type Point struct {
X, Y int32
}
type MouseEvent struct {
Message uint32
Point Point
Data uint32
}
type KeyboardEvent struct {
Message uint32
VKCode uint32
ScanCode uint32
Flags uint32
}
type InputHandler interface {
Install(ctx context.Context, onMouse func(MouseEvent) bool, onKey func(KeyboardEvent) bool) error
Uninstall()
SetCursorPos(x, y int32) bool
ShowCursor(show bool)
GetScreenResolution() (int32, int32)
}

View File

@ -0,0 +1,171 @@
//go:build windows
package input
import (
"context"
"fmt"
"runtime"
"unsafe"
"golang.org/x/sys/windows"
)
var (
user32 = windows.NewLazySystemDLL("user32.dll")
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
procSetWindowsHookExW = user32.NewProc("SetWindowsHookExW")
procUnhookWindowsHookEx = user32.NewProc("UnhookWindowsHookEx")
procCallNextHookEx = user32.NewProc("CallNextHookEx")
procGetMessageW = user32.NewProc("GetMessageW")
procSetCursorPos = user32.NewProc("SetCursorPos")
procShowCursor = user32.NewProc("ShowCursor")
procGetModuleHandleW = kernel32.NewProc("GetModuleHandleW")
procGetSystemMetrics = user32.NewProc("GetSystemMetrics")
procSetProcessDPIAware = user32.NewProc("SetProcessDPIAware")
procPostThreadMessageW = user32.NewProc("PostThreadMessageW")
)
const (
WH_KEYBOARD_LL = 13
WH_MOUSE_LL = 14
SM_CXSCREEN = 0
SM_CYSCREEN = 1
WM_QUIT = 0x0012
)
func init() {
procSetProcessDPIAware.Call()
}
type MSLLHOOKSTRUCT struct {
Pt Point
MouseData uint32 // Mantemos uint32 mas vamos converter no callback
Flags uint32
Time uint32
DwExtraInfo uintptr
}
type KBDLLHOOKSTRUCT struct {
VkCode uint32
ScanCode uint32
Flags uint32
Time uint32
DwExtraInfo uintptr
}
type windowsInputHandler struct {
mouseHook uintptr
keyHook uintptr
tid uint32
}
func NewInputHandler() InputHandler {
return &windowsInputHandler{}
}
func (h *windowsInputHandler) Install(ctx context.Context, onMouse func(MouseEvent) bool, onKey func(KeyboardEvent) bool) error {
ready := make(chan error, 1)
go func() {
runtime.LockOSThread()
h.tid = windows.GetCurrentThreadId()
hMod, _, _ := procGetModuleHandleW.Call(0)
mouseCallback := windows.NewCallback(func(nCode int, wParam uintptr, lParam uintptr) uintptr {
if nCode >= 0 {
info := (*MSLLHOOKSTRUCT)(unsafe.Pointer(lParam))
ev := MouseEvent{
Message: uint32(wParam),
Point: info.Pt,
Data: info.MouseData,
}
// Se a engine tratar o evento, não passamos para o próximo hook
if onMouse(ev) {
return 1
}
}
ret, _, _ := procCallNextHookEx.Call(h.mouseHook, uintptr(nCode), wParam, lParam)
return ret
})
keyCallback := windows.NewCallback(func(nCode int, wParam uintptr, lParam uintptr) uintptr {
if nCode >= 0 {
info := (*KBDLLHOOKSTRUCT)(unsafe.Pointer(lParam))
ev := KeyboardEvent{
Message: uint32(wParam),
VKCode: info.VkCode,
ScanCode: info.ScanCode,
Flags: info.Flags,
}
if onKey(ev) {
return 1
}
}
ret, _, _ := procCallNextHookEx.Call(h.keyHook, uintptr(nCode), wParam, lParam)
return ret
})
mh, _, _ := procSetWindowsHookExW.Call(WH_MOUSE_LL, mouseCallback, hMod, 0)
if mh == 0 {
ready <- fmt.Errorf("failed mouse hook")
return
}
h.mouseHook = mh
kh, _, _ := procSetWindowsHookExW.Call(WH_KEYBOARD_LL, keyCallback, hMod, 0)
if kh == 0 {
ready <- fmt.Errorf("failed key hook")
return
}
h.keyHook = kh
ready <- nil
var msg struct {
Hwnd windows.Handle
Message uint32
WParam uintptr
LParam uintptr
Time uint32
Pt Point
}
for {
ret, _, _ := procGetMessageW.Call(uintptr(unsafe.Pointer(&msg)), 0, 0, 0)
if ret == 0 || msg.Message == WM_QUIT {
break
}
}
procUnhookWindowsHookEx.Call(h.mouseHook)
procUnhookWindowsHookEx.Call(h.keyHook)
}()
return <-ready
}
func (h *windowsInputHandler) Uninstall() {
if h.tid != 0 {
procPostThreadMessageW.Call(uintptr(h.tid), WM_QUIT, 0, 0)
}
}
func (h *windowsInputHandler) SetCursorPos(x, y int32) bool {
ret, _, _ := procSetCursorPos.Call(uintptr(x), uintptr(y))
return ret != 0
}
func (h *windowsInputHandler) ShowCursor(show bool) {
s := -1 // No Windows, ShowCursor(FALSE) decrementa um contador
if show {
s = 1
}
// Vamos usar uma abordagem mais direta se necessário, mas por enquanto:
procShowCursor.Call(uintptr(s))
}
func (h *windowsInputHandler) GetScreenResolution() (int32, int32) {
w, _, _ := procGetSystemMetrics.Call(SM_CXSCREEN)
h_res, _, _ := procGetSystemMetrics.Call(SM_CYSCREEN)
return int32(w), int32(h_res)
}

340
internal/kvm/engine.go Normal file
View File

@ -0,0 +1,340 @@
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 Engine struct {
mu sync.Mutex
transport transport.Transport
inputHandler input.InputHandler
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
}
func NewEngine(t transport.Transport, h input.InputHandler) *Engine {
return &Engine{
transport: t,
inputHandler: h,
clientPos: PosRight,
}
}
func (e *Engine) Transport() transport.Transport {
return e.transport
}
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))
return e.inputHandler.Install(ctx, e.onMouse, e.onKey)
}
func (e *Engine) processarScroll(data uint32) {
e.scrollActive = true
e.scrollTimer = time.Now()
delta := int32(int16(data >> 16))
e.wheelAccum += delta
const Divisor = 40 // Bem sensível para touchpad
toSend := e.wheelAccum / Divisor
if toSend != 0 {
e.wheelAccum -= toSend * Divisor
err := e.transport.Send([]byte{'W', byte(int8(clamp(int(toSend), -127, 127)))})
if err == nil {
LogDebug(fmt.Sprintf("SCROLL: delta=%d enviado=%d", delta, toSend))
}
}
}
func (e *Engine) onMouse(ev input.MouseEvent) bool {
e.mu.Lock()
defer e.mu.Unlock()
if !e.transport.IsConnected() {
return false
}
if !e.clientMode {
if ev.Message == 0x0200 && e.isAtExitEdge(ev.Point) {
e.enterClientMode(ev.Point)
return true
}
return false
}
// ─── MODO CLIENTE ATIVO ───
// Log de qualquer evento que não seja movimento simples (para descobrir o ID do touchpad)
if ev.Message != 0x0200 {
LogDebug(fmt.Sprintf("Evento Mouse: 0x%X | Data: %d", ev.Message, ev.Data))
}
switch ev.Message {
case 0x020A, 0x020E: // Roda Vertical ou Horizontal
e.processarScroll(ev.Data)
return true
case 0x0200: // Move
if e.isWarping { e.isWarping = false; return true }
if e.scrollActive {
if time.Since(e.scrollTimer) > 300*time.Millisecond {
e.scrollActive = false
e.virtualX, e.virtualY = 0, 0
}
e.lastRawPos = ev.Point
return true
}
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
return true
}
w, h := e.inputHandler.GetScreenResolution()
e.isWarping = true
e.inputHandler.SetCursorPos(w/2, h/2)
e.lastRawPos = input.Point{X: w / 2, Y: h / 2}
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)})
}
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()
w, h := e.inputHandler.GetScreenResolution()
e.isWarping = true
e.inputHandler.SetCursorPos(w/2, h/2)
e.lastRawPos = input.Point{X: w / 2, Y: h / 2}
e.inputHandler.ShowCursor(false)
e.transport.Send([]byte{'O'})
}
func (e *Engine) exitClientMode() {
LogDebug("Saindo Modo Cliente.")
e.clientMode = false
e.lastModeChange = time.Now()
e.inputHandler.ShowCursor(true)
w, h := e.inputHandler.GetScreenResolution()
var ret input.Point
const Offset = 120
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) 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
}

View File

@ -0,0 +1,146 @@
package transport
import (
"context"
"errors"
"fmt"
"sync"
"time"
"tinygo.org/x/bluetooth"
)
var (
NusServiceUUID = bluetooth.NewUUID([16]byte{0x6e, 0x40, 0x00, 0x01, 0xb5, 0xa3, 0xf3, 0x93, 0xe0, 0xa9, 0xe5, 0x0e, 0x24, 0xdc, 0xca, 0x9e})
NusRxUUID = bluetooth.NewUUID([16]byte{0x6e, 0x40, 0x00, 0x02, 0xb5, 0xa3, 0xf3, 0x93, 0xe0, 0xa9, 0xe5, 0x0e, 0x24, 0xdc, 0xca, 0x9e})
)
type RealBleTransport struct {
adapter *bluetooth.Adapter
device bluetooth.Device
rxChar bluetooth.DeviceCharacteristic
connected bool
mu sync.Mutex
address bluetooth.Address
found bool
}
func NewBleTransport() *RealBleTransport {
return &RealBleTransport{
adapter: bluetooth.DefaultAdapter,
}
}
func (t *RealBleTransport) Detect(ctx context.Context) (bool, error) {
t.mu.Lock()
if t.found {
t.mu.Unlock()
return true, nil
}
t.mu.Unlock()
fmt.Println("[BLE] Iniciando Scan...")
if err := t.adapter.Enable(); err != nil {
return false, err
}
foundChan := make(chan bluetooth.Address, 1)
err := t.adapter.Scan(func(adapter *bluetooth.Adapter, result bluetooth.ScanResult) {
if result.LocalName() == "KVMote" {
adapter.StopScan()
foundChan <- result.Address
}
})
if err != nil {
return false, err
}
select {
case addr := <-foundChan:
t.mu.Lock()
t.address = addr
t.found = true
t.mu.Unlock()
fmt.Println("[BLE] Encontrado!")
return true, nil
case <-ctx.Done():
t.adapter.StopScan()
return false, ctx.Err()
case <-time.After(5 * time.Second):
t.adapter.StopScan()
return false, errors.New("não encontrado")
}
}
func (t *RealBleTransport) Connect(ctx context.Context) error {
t.mu.Lock()
addr := t.address
t.mu.Unlock()
fmt.Println("[BLE] Conectando...")
device, err := t.adapter.Connect(addr, bluetooth.ConnectionParams{})
if err != nil {
return err
}
t.mu.Lock()
t.device = device
t.mu.Unlock()
fmt.Println("[BLE] Buscando Serviço...")
services, err := device.DiscoverServices([]bluetooth.UUID{NusServiceUUID})
if err != nil || len(services) == 0 {
return errors.New("serviço não encontrado")
}
fmt.Println("[BLE] Buscando RX...")
chars, err := services[0].DiscoverCharacteristics([]bluetooth.UUID{NusRxUUID})
if err != nil || len(chars) == 0 {
return errors.New("característica não encontrada")
}
t.mu.Lock()
t.rxChar = chars[0]
t.connected = true
t.mu.Unlock()
fmt.Println("[BLE] PRONTO!")
t.Send([]byte{'H'})
return nil
}
func (t *RealBleTransport) Disconnect() error {
t.mu.Lock()
defer t.mu.Unlock()
if !t.connected { return nil }
// Proteção contra crash no Windows
go func() {
defer func() { recover() }()
t.device.Disconnect()
}()
t.connected = false
return nil
}
func (t *RealBleTransport) IsConnected() bool {
t.mu.Lock()
defer t.mu.Unlock()
return t.connected
}
func (t *RealBleTransport) Send(data []byte) error {
t.mu.Lock()
defer t.mu.Unlock()
if !t.connected { return nil }
_, err := t.rxChar.WriteWithoutResponse(data)
return err
}
func (t *RealBleTransport) SendLossy(data []byte) error {
return t.Send(data)
}
func (t *RealBleTransport) DeviceLabel() string { return "KVMote (BLE)" }
func (t *RealBleTransport) ClipboardConfig() (int, int, bool) { return 65536, 5, true }

View File

@ -0,0 +1,16 @@
package transport
import (
"context"
)
type Transport interface {
Detect(ctx context.Context) (bool, error)
Connect(ctx context.Context) error
Disconnect() error
IsConnected() bool
Send(data []byte) error
SendLossy(data []byte) error
DeviceLabel() string
ClipboardConfig() (int, int, bool) // maxChars, delayMs, supportsBatch
}

409
kvmote_debug.log Normal file
View File

@ -0,0 +1,409 @@
[11:57:49.995] Engine Iniciada. Resolução: 2880x1800. Posição Cliente: 2
[11:58:01.505] Engine Iniciada. Resolução: 2880x1800. Posição Cliente: 2
[12:00:07.989] Engine Iniciada. Resolução: 2880x1800. Posição Cliente: 2
[12:00:14.765] Engine Iniciada. Resolução: 2880x1800. Posição Cliente: 2
[12:01:21.879] Engine Iniciada. Resolução: 2880x1800. Posição Cliente: 2
[12:01:56.100] Engine Iniciada. Resolução: 2880x1800. Posição Cliente: 2
[12:03:09.822] Engine Iniciada. Resolução: 2880x1800. Posição Cliente: 2
[12:03:16.104] Engine Iniciada. Resolução: 2880x1800. Posição Cliente: 2
[12:03:23.314] Mouse: (2800, 921) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:23.315] Mouse: (2800, 921) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:23.318] Mouse: (2820, 916) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:23.318] Mouse: (2820, 916) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:23.322] Mouse: (2841, 909) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:23.323] Mouse: (2841, 909) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:23.327] Mouse: (2861, 906) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:23.327] BORDA DETECTADA em (2861, 906). Entrando em Modo Cliente.
[12:03:35.377] RETORNO AO HOST. Virtual: (-42, -18).
[12:03:37.803] Mouse: (2783, 956) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:37.803] Mouse: (2783, 956) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:37.807] Mouse: (2795, 954) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:37.807] Mouse: (2795, 954) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:37.811] Mouse: (2814, 952) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:37.811] Mouse: (2814, 952) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:37.815] Mouse: (2839, 948) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:37.815] Mouse: (2839, 948) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:37.819] Mouse: (2865, 946) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:37.819] BORDA DETECTADA em (2865, 946). Entrando em Modo Cliente.
[12:03:39.322] RETORNO AO HOST. Virtual: (-59, -54).
[12:03:42.688] Mouse: (2784, 936) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:42.689] Mouse: (2784, 936) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:42.693] Mouse: (2798, 935) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:42.693] Mouse: (2798, 935) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:42.697] Mouse: (2812, 935) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:42.697] Mouse: (2812, 935) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:42.701] Mouse: (2824, 935) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:42.701] Mouse: (2824, 935) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:42.705] Mouse: (2836, 935) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:42.705] Mouse: (2836, 935) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:42.709] Mouse: (2846, 935) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:42.709] Mouse: (2846, 935) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:42.713] Mouse: (2856, 935) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:42.714] BORDA DETECTADA em (2856, 935). Entrando em Modo Cliente.
[12:03:46.594] RETORNO AO HOST. Virtual: (-32, 84).
[12:03:48.628] Mouse: (2784, 872) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.628] Mouse: (2784, 872) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.632] Mouse: (2790, 872) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.633] Mouse: (2790, 872) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.636] Mouse: (2795, 872) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.636] Mouse: (2795, 872) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.640] Mouse: (2798, 872) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.641] Mouse: (2798, 872) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.644] Mouse: (2802, 872) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.644] Mouse: (2802, 872) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.649] Mouse: (2806, 872) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.649] Mouse: (2806, 872) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.653] Mouse: (2808, 872) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.653] Mouse: (2808, 872) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.656] Mouse: (2811, 872) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.657] Mouse: (2811, 872) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.661] Mouse: (2813, 872) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.662] Mouse: (2813, 872) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.665] Mouse: (2815, 873) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.665] Mouse: (2815, 873) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.669] Mouse: (2817, 873) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.669] Mouse: (2817, 873) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.673] Mouse: (2817, 873) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.673] Mouse: (2817, 873) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.677] Mouse: (2819, 873) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.678] Mouse: (2819, 873) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.681] Mouse: (2820, 873) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.681] Mouse: (2820, 873) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.685] Mouse: (2821, 874) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.686] Mouse: (2821, 874) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.690] Mouse: (2822, 874) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.690] Mouse: (2822, 874) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.694] Mouse: (2823, 874) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.694] Mouse: (2823, 874) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.698] Mouse: (2823, 875) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.698] Mouse: (2823, 875) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.993] Mouse: (2834, 873) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.994] Mouse: (2834, 873) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.997] Mouse: (2852, 872) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:48.997] BORDA DETECTADA em (2852, 872). Entrando em Modo Cliente.
[12:03:51.189] RETORNO AO HOST. Virtual: (-31, 46).
[12:03:53.889] Mouse: (2788, 872) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:53.889] Mouse: (2788, 872) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:53.892] Mouse: (2807, 872) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:53.893] Mouse: (2807, 872) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:53.897] Mouse: (2827, 874) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:53.898] Mouse: (2827, 874) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:53.902] Mouse: (2845, 874) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:53.902] Mouse: (2845, 874) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:53.906] Mouse: (2861, 876) | Screen: (2880, 1800) | Pos Alvo: 2
[12:03:53.906] BORDA DETECTADA em (2861, 876). Entrando em Modo Cliente.
[12:03:56.084] RETORNO AO HOST. Virtual: (-37, -4).
[12:04:02.762] Posição alterada para: 1
[12:04:05.242] Engine Iniciada. Resolução: 2880x1800. Posição Cliente: 1
[12:04:16.367] Mouse: (96, 880) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:16.367] Mouse: (96, 880) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:16.368] Mouse: (96, 880) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:16.370] Mouse: (76, 880) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:16.371] Mouse: (76, 880) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:16.371] Mouse: (76, 880) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:16.375] Mouse: (56, 880) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:16.376] Mouse: (56, 880) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:16.376] Mouse: (56, 880) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:16.380] Mouse: (37, 880) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:16.380] Mouse: (37, 880) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:16.381] Mouse: (37, 880) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:16.382] Mouse: (17, 879) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:16.383] Mouse: (17, 879) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:16.383] Mouse: (17, 879) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:16.387] Mouse: (-1, 878) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:16.387] BORDA DETECTADA em (-1, 878). Entrando em Modo Cliente.
[12:04:17.431] RETORNO AO HOST. Virtual: (31, 4).
[12:04:19.412] Mouse: (97, 869) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.413] Mouse: (97, 869) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.413] Mouse: (97, 869) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.416] Mouse: (89, 868) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.417] Mouse: (89, 868) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.417] Mouse: (89, 868) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.421] Mouse: (80, 867) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.421] Mouse: (80, 867) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.421] Mouse: (80, 867) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.424] Mouse: (74, 867) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.425] Mouse: (74, 867) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.425] Mouse: (74, 867) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.429] Mouse: (68, 865) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.429] Mouse: (68, 865) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.429] Mouse: (68, 865) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.433] Mouse: (61, 865) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.433] Mouse: (61, 865) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.434] Mouse: (61, 865) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.437] Mouse: (57, 864) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.438] Mouse: (57, 864) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.438] Mouse: (57, 864) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.441] Mouse: (52, 864) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.441] Mouse: (52, 864) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.441] Mouse: (52, 864) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.445] Mouse: (49, 863) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.445] Mouse: (49, 863) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.445] Mouse: (49, 863) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.450] Mouse: (46, 863) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.450] Mouse: (46, 863) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.451] Mouse: (46, 863) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.453] Mouse: (42, 862) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.454] Mouse: (42, 862) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.454] Mouse: (42, 862) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.458] Mouse: (39, 861) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.458] Mouse: (39, 861) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.458] Mouse: (39, 861) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.462] Mouse: (36, 861) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.462] Mouse: (36, 861) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.463] Mouse: (36, 861) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.465] Mouse: (34, 861) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.466] Mouse: (34, 861) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.466] Mouse: (34, 861) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.470] Mouse: (31, 860) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.471] Mouse: (31, 860) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.471] Mouse: (31, 860) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.474] Mouse: (30, 860) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.474] Mouse: (30, 860) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.474] Mouse: (30, 860) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.478] Mouse: (28, 860) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.478] Mouse: (28, 860) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.479] Mouse: (28, 860) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.482] Mouse: (26, 859) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.482] Mouse: (26, 859) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.482] Mouse: (26, 859) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.487] Mouse: (25, 859) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.488] Mouse: (25, 859) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.488] Mouse: (25, 859) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.491] Mouse: (24, 858) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.491] Mouse: (24, 858) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.491] Mouse: (24, 858) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.494] Mouse: (23, 858) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.495] Mouse: (23, 858) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.495] Mouse: (23, 858) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.498] Mouse: (23, 858) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.499] Mouse: (23, 858) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.499] Mouse: (23, 858) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.503] Mouse: (22, 858) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.503] Mouse: (22, 858) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.503] Mouse: (22, 858) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.507] Mouse: (21, 857) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.507] Mouse: (21, 857) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.507] Mouse: (21, 857) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.516] Mouse: (20, 857) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.516] Mouse: (20, 857) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.517] Mouse: (20, 857) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.523] Mouse: (19, 857) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.523] Mouse: (19, 857) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.523] Mouse: (19, 857) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.527] Mouse: (18, 857) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.528] Mouse: (18, 857) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.528] Mouse: (18, 857) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.531] Mouse: (18, 856) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.532] Mouse: (18, 856) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.532] Mouse: (18, 856) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.545] Mouse: (18, 855) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.545] Mouse: (18, 855) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.545] Mouse: (18, 855) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.897] Mouse: (10, 854) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.898] Mouse: (10, 854) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.898] Mouse: (10, 854) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.902] Mouse: (-4, 853) | Screen: (2880, 1800) | Pos Alvo: 1
[12:04:19.902] BORDA DETECTADA em (-4, 853). Entrando em Modo Cliente.
[12:04:21.156] RETORNO AO HOST. Virtual: (31, 7).
[12:08:55.556] Engine Iniciada. Resolução: 2880x1800. Posição Cliente: 2
[12:09:07.216] Engine Iniciada. Resolução: 2880x1800. Posição Cliente: 2
[12:09:11.273] BORDA DETECTADA em (2881, 768). Entrando em Modo Cliente.
[12:09:11.274] Executando enterClientMode...
[12:09:13.855] RETORNO AO HOST. Virtual: (-104, 62).
[12:09:13.855] Saindo do Modo Cliente (Retornando ao Host).
[12:09:18.233] BORDA DETECTADA em (2875, 910). Entrando em Modo Cliente.
[12:09:18.233] Executando enterClientMode...
[12:09:28.239] RETORNO AO HOST. Virtual: (-103, -73).
[12:09:28.240] Saindo do Modo Cliente (Retornando ao Host).
[12:09:48.125] Posição alterada para: 1
[12:09:50.114] Engine Iniciada. Resolução: 2880x1800. Posição Cliente: 1
[12:09:53.858] BORDA DETECTADA em (-5, 814). Entrando em Modo Cliente.
[12:09:53.858] Executando enterClientMode...
[12:11:23.447] RETORNO AO HOST. Virtual: (109, 340).
[12:11:23.448] Saindo do Modo Cliente (Retornando ao Host).
[12:11:39.432] Engine Iniciada. Resolução: 2880x1800. Posição Cliente: 2
[12:11:43.252] Posição alterada para: 1
[12:11:45.237] Engine Iniciada. Resolução: 2880x1800. Posição Cliente: 1
[12:11:58.482] Engine Iniciada. Resolução: 2880x1800. Posição Cliente: 1
[12:12:26.021] BORDA DETECTADA em (-5, 1029). Entrando em Modo Cliente.
[12:12:26.021] Executando enterClientMode...
[12:13:10.104] RETORNO AO HOST. Virtual: (102, -89).
[12:13:10.105] Saindo do Modo Cliente (Retornando ao Host).
[12:13:18.341] BORDA DETECTADA em (-11, 921). Entrando em Modo Cliente.
[12:13:18.341] Executando enterClientMode...
[12:13:27.416] RETORNO AO HOST. Virtual: (108, -48).
[12:13:27.416] Saindo do Modo Cliente (Retornando ao Host).
[12:38:10.026] Engine Iniciada. Resolução: 2880x1800. Posição Cliente: 2
[12:38:21.182] Engine Iniciada. Resolução: 2880x1800. Posição Cliente: 2
[12:38:47.459] BORDA DETECTADA em (2878, 867). Entrando em Modo Cliente.
[12:38:47.459] Entrando Modo Cliente. EdgeEntry: (2878, 867)
[12:38:49.941] RETORNO AO HOST. Virtual: (-102, 50).
[12:38:49.941] Saindo Modo Cliente...
[12:38:50.862] BORDA DETECTADA em (2877, 869). Entrando em Modo Cliente.
[12:38:50.862] Entrando Modo Cliente. EdgeEntry: (2877, 869)
[12:39:12.965] RETORNO AO HOST. Virtual: (-108, 78).
[12:39:12.965] Saindo Modo Cliente...
[12:40:05.307] Engine Iniciada. Resolução: 2880x1800. Posição Cliente: 2
[12:40:10.496] Posição alterada para: 1
[12:40:15.610] Engine Iniciada. Resolução: 2880x1800. Posição Cliente: 1
[12:40:19.932] BORDA DETECTADA em (-1, 850). Entrando em Modo Cliente.
[12:40:19.932] Entrando Modo Cliente. EdgeEntry: (-1, 850)
[12:40:22.694] RETORNO AO HOST. Virtual: (109, 9).
[12:40:22.694] Saindo Modo Cliente...
[12:40:23.511] BORDA DETECTADA em (-11, 915). Entrando em Modo Cliente.
[12:40:23.511] Entrando Modo Cliente. EdgeEntry: (-11, 915)
[12:40:25.789] RETORNO AO HOST. Virtual: (101, -7).
[12:40:25.789] Saindo Modo Cliente...
[12:40:27.412] BORDA DETECTADA em (-10, 866). Entrando em Modo Cliente.
[12:40:27.412] Entrando Modo Cliente. EdgeEntry: (-10, 866)
[12:40:53.410] RETORNO AO HOST. Virtual: (111, 303).
[12:40:53.410] Saindo Modo Cliente...
[12:40:56.329] BORDA DETECTADA em (-6, 842). Entrando em Modo Cliente.
[12:40:56.329] Entrando Modo Cliente. EdgeEntry: (-6, 842)
[12:46:51.892] RETORNO AO HOST. Virtual: (125, 982).
[12:46:51.892] Saindo Modo Cliente...
[12:48:18.098] Ctrl+C detectado no Host. Clipboard marcado.
[12:48:18.098] Ctrl+C detectado no Host. Clipboard marcado.
[12:48:52.272] Ctrl+C detectado no Host. Clipboard marcado.
[12:48:52.273] Ctrl+C detectado no Host. Clipboard marcado.
[12:48:52.458] Ctrl+C detectado no Host. Clipboard marcado.
[12:48:52.459] Ctrl+C detectado no Host. Clipboard marcado.
[12:48:52.640] Ctrl+C detectado no Host. Clipboard marcado.
[12:48:52.640] Ctrl+C detectado no Host. Clipboard marcado.
[12:48:59.721] BORDA DETECTADA em (-10, 973). Entrando em Modo Cliente.
[12:48:59.722] Entrando Modo Cliente. EdgeEntry: (-10, 973)
[12:49:09.111] Ctrl+V detectado no Cliente. Iniciando Batch Paste...
[13:20:24.355] RETORNO AO HOST. Virtual: (130, 757).
[13:20:24.355] Saindo Modo Cliente...
[09:28:10.941] Engine Iniciada. Tela: 2880x1800. Pos: 2
[09:28:52.395] Engine Iniciada. Tela: 2880x1800. Pos: 2
[09:29:03.702] Engine Iniciada. Tela: 2880x1800. Pos: 2
[09:29:19.095] Entrando em Modo Cliente. Pos: (2906, 949)
[09:29:21.721] Saindo do Modo Cliente.
[09:29:22.884] Entrando em Modo Cliente. Pos: (2865, 860)
[09:29:27.340] Saindo do Modo Cliente.
[09:29:28.901] Entrando em Modo Cliente. Pos: (2868, 819)
[09:29:30.808] Saindo do Modo Cliente.
[09:30:17.696] Engine Iniciada. Tela: 2880x1800. Pos: 2
[09:30:22.777] Posição alterada para: 1
[09:30:33.630] Engine Iniciada. Tela: 2880x1800. Pos: 1
[09:42:33.108] Engine Iniciada. Tela: 2880x1800. Pos: 2
[09:42:38.444] Posição alterada para: 1
[09:42:43.199] Engine Iniciada. Tela: 2880x1800. Pos: 1
[09:42:46.978] Entrando em Modo Cliente. Pos: (-2, 865)
[09:42:49.387] Saindo do Modo Cliente.
[09:42:50.592] Entrando em Modo Cliente. Pos: (-6, 898)
[09:42:52.761] Saindo do Modo Cliente.
[09:42:54.791] Entrando em Modo Cliente. Pos: (-8, 942)
[09:42:59.488] Saindo do Modo Cliente.
[10:22:25.896] Engine Iniciada. Tela: 2880x1800. Pos: 2
[10:22:31.500] Posição alterada para: 1
[10:22:36.437] Engine Iniciada. Tela: 2880x1800. Pos: 1
[10:22:49.156] Entrando em Modo Cliente. Pos: (-1, 843)
[10:23:18.364] RETORNO AO HOST. VirtualX: 609. Pos: 1
[10:23:18.364] Saindo do Modo Cliente.
[10:23:32.467] Entrando em Modo Cliente. Pos: (-5, 848)
[10:23:36.996] RETORNO AO HOST. VirtualX: 607. Pos: 1
[10:23:36.998] Saindo do Modo Cliente.
[10:24:26.939] Engine Iniciada. Tela: 2880x1800. Pos: 2
[10:24:30.229] Posição alterada para: 1
[10:24:34.912] Engine Iniciada. Tela: 2880x1800. Pos: 1
[10:24:42.439] Entrando em Modo Cliente. Pos: (-14, 884)
[10:28:02.965] RETORNO AO HOST. VirtualX: 605. Pos: 1
[10:28:02.965] Saindo do Modo Cliente.
[11:05:31.633] Engine Iniciada. Tela: 2880x1800. Pos: 2
[11:05:34.898] Posição alterada para: 1
[11:05:39.630] Engine Iniciada. Tela: 2880x1800. Pos: 1
[11:06:44.696] Engine Iniciada. Tela: 2880x1800. Pos: 2
[11:06:48.495] Posição alterada para: 1
[11:06:55.700] Engine Iniciada. Tela: 2880x1800. Pos: 1
[11:07:00.222] Entrando em Modo Cliente. Pos: (-6, 891)
[11:07:14.399] RETORNO AO HOST. VirtualX: 620. Pos: 1
[11:07:14.399] Saindo do Modo Cliente.
[11:07:24.430] Entrando em Modo Cliente. Pos: (-5, 846)
[11:08:27.457] RETORNO AO HOST. VirtualX: 618. Pos: 1
[11:08:27.458] Saindo do Modo Cliente.
[11:14:48.874] Engine Iniciada. Tela: 2880x1800. Pos: 2
[11:19:37.432] Engine Iniciada. Tela: 2880x1800. Pos: 2
[11:19:46.914] Posição alterada para: 1
[11:19:50.268] Engine Iniciada. Tela: 2880x1800. Pos: 1
[11:19:55.967] Entrando Modo Cliente. Pos: (-9, 837)
[11:20:13.991] RETORNO AO HOST. VirtualX: 624
[11:20:13.992] Saindo do Modo Cliente.
[11:22:44.004] Engine Iniciada. Tela: 2880x1800. Pos: 2
[11:22:48.488] Engine Iniciada. Tela: 2880x1800. Pos: 1
[11:22:52.166] Entrando Modo Cliente. Pos: (-13, 806)
[11:22:59.135] Saindo Modo Cliente. VirtualX acumulado: 603
[11:27:30.261] Engine Iniciada. Tela: 2880x1800. Pos: 2
[11:27:35.447] Engine Iniciada. Tela: 2880x1800. Pos: 1
[11:28:35.980] Engine Iniciada. Tela: 2880x1800. Pos: 2
[11:29:09.690] Engine Iniciada. Tela: 2880x1800. Pos: 1
[11:29:14.965] Entrando Modo Cliente. Pos: (-14, 813)
[11:29:33.834] Saindo Modo Cliente.
[11:51:58.180] Engine Iniciada. Tela: 2880x1800. Pos: 2
[11:52:03.638] Engine Iniciada. Tela: 2880x1800. Pos: 1
[11:52:24.812] Engine Iniciada. Tela: 2880x1800. Pos: 2
[11:53:02.198] Engine Iniciada. Tela: 2880x1800. Pos: 1
[11:53:51.757] Engine Iniciada. Tela: 2880x1800. Pos: 1
[11:54:18.283] Engine Iniciada. Tela: 2880x1800. Pos: 1
[11:54:35.205] Engine Iniciada. Tela: 2880x1800. Pos: 2
[11:54:40.373] Engine Iniciada. Tela: 2880x1800. Pos: 1
[15:35:23.555] Engine Iniciada. Tela: 2880x1800. Pos: 2
[15:36:24.111] Engine Iniciada. Tela: 2880x1800. Pos: 1
[16:09:14.063] Engine Iniciada. Tela: 2880x1800. Pos: 2
[16:10:56.893] Engine Iniciada. Tela: 2880x1800. Pos: 1
[16:12:09.289] Engine Iniciada. Tela: 2880x1800. Pos: 2
[16:13:26.059] Engine Iniciada. Tela: 2880x1800. Pos: 2
[16:13:40.844] Engine Iniciada. Tela: 2880x1800. Pos: 1
[16:22:44.207] Engine Iniciada. Tela: 2880x1800. Pos: 2
[16:31:24.976] Engine Iniciada. Tela: 2880x1800. Pos: 2
[16:32:03.021] Engine Iniciada. Tela: 2880x1800. Pos: 1
[16:48:16.283] Engine Iniciada. Tela: 2880x1800. Pos: 2
[16:48:21.459] Engine Iniciada. Tela: 2880x1800. Pos: 1
[16:52:15.440] Engine Iniciada. Tela: 2880x1800. Pos: 2
[16:52:21.547] Engine Iniciada. Tela: 2880x1800. Pos: 1
[16:52:25.780] Entrando Modo Cliente em (-4, 938)
[16:52:31.097] Saindo Modo Cliente.
[17:06:27.261] Engine Iniciada. Tela: 2880x1800. Pos: 2
[17:06:42.837] Engine Iniciada. Tela: 2880x1800. Pos: 1
[17:07:16.443] Engine Iniciada. Tela: 2880x1800. Pos: 1
[17:08:11.678] Entrando Modo Cliente em (-10, 1162)
[17:08:15.576] Evento Mouse: 0x201 | Data: 0
[17:08:15.795] Evento Mouse: 0x202 | Data: 0
[17:08:18.563] Saindo Modo Cliente.
[17:25:17.509] Engine Iniciada. Tela: 2880x1800. Pos: 2
[17:25:46.407] Engine Iniciada. Tela: 2880x1800. Pos: 1
[17:37:23.957] Engine Iniciada. Tela: 2880x1800. Pos: 1
[17:37:27.573] Entrando Modo Cliente em (-10, 872)
[17:37:29.944] Evento Mouse: 0x201 | Data: 0
[17:37:30.166] Evento Mouse: 0x202 | Data: 0
[17:37:33.920] Saindo Modo Cliente.
[17:51:08.792] Engine Iniciada. Tela: 2880x1800. Pos: 2
[17:52:15.999] Engine Iniciada. Tela: 2880x1800. Pos: 1
[09:45:58.258] Engine Iniciada. Tela: 2880x1800. Pos: 2
[10:01:58.140] Engine Iniciada. Tela: 2880x1800. Pos: 2
[10:02:03.298] App: Chamando SendCtrlAltDel
[10:02:03.298] Enviando CTRL+ALT+DEL...
[10:02:03.298] Erro: Transporte não conectado.
[10:02:06.169] App: Chamando SendCtrlAltDel
[10:02:06.170] Enviando CTRL+ALT+DEL...
[10:02:06.170] Erro: Transporte não conectado.
[10:02:08.010] Engine Iniciada. Tela: 2880x1800. Pos: 2
[10:02:12.417] App: Chamando SendCtrlAltDel
[10:02:12.417] Enviando CTRL+ALT+DEL...
[10:02:12.581] Sequência CTRL+ALT+DEL enviada.
[10:02:15.538] Entrando Modo Cliente em (2879, 727)
[10:02:17.162] Evento Mouse: 0x201 | Data: 0
[10:02:17.378] Evento Mouse: 0x202 | Data: 0
[10:02:18.765] Saindo Modo Cliente.
[10:02:21.762] Entrando Modo Cliente em (2890, 954)
[10:02:28.196] Saindo Modo Cliente.
[10:02:35.501] Entrando Modo Cliente em (2876, 888)
[10:02:38.211] Evento Mouse: 0x201 | Data: 0
[10:02:38.429] Evento Mouse: 0x202 | Data: 0
[10:02:39.707] Evento Mouse: 0x201 | Data: 0
[10:02:39.926] Evento Mouse: 0x202 | Data: 0
[10:02:44.791] Saindo Modo Cliente.
[10:02:46.049] Entrando Modo Cliente em (2897, 760)
[10:02:48.162] Saindo Modo Cliente.

36
main.go Normal file
View File

@ -0,0 +1,36 @@
package main
import (
"embed"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
)
//go:embed all:frontend/dist
var assets embed.FS
func main() {
// Create an instance of the app structure
app := NewApp()
// Create application with options
err := wails.Run(&options.App{
Title: "KVMote",
Width: 400,
Height: 550,
AssetServer: &assetserver.Options{
Assets: assets,
},
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
OnStartup: app.startup,
Bind: []interface{}{
app,
},
})
if err != nil {
println("Error:", err.Error())
}
}

20
wails.json Normal file
View File

@ -0,0 +1,20 @@
{
"name": "KVMote",
"assetdir": "frontend/dist",
"frontend:install": "",
"frontend:build": "",
"frontend:dev:watcher": "",
"frontend:dev:serverUrl": "",
"author": {
"name": "KVMote Team",
"email": ""
},
"wailsjsdir": "frontend/src/wailsjs",
"outputfilename": "kvmote",
"info": {
"productName": "KVMote",
"productVersion": "1.0.0",
"copyright": "Copyright 2026",
"comments": "KVM over Bluetooth Bridge"
}
}