commit b19e63f84783fcf90c6d336d0fb75c0e4f4b6648 Author: Ricardo Carneiro Date: Fri Apr 24 10:10:17 2026 -0300 Initial commit with .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..26f6955 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..6ab0054 --- /dev/null +++ b/.vscode/launch.json @@ -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" + } + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..24b0bae --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "go.buildTags": "desktop,debug", + "go.lintFlags": ["--tags", "desktop,debug"] +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..089e200 --- /dev/null +++ b/CLAUDE.md @@ -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 diff --git a/app.go b/app.go new file mode 100644 index 0000000..c309d5b --- /dev/null +++ b/app.go @@ -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) +} diff --git a/build/appicon.png b/build/appicon.png new file mode 100644 index 0000000..63617fe Binary files /dev/null and b/build/appicon.png differ diff --git a/build/windows/icon.ico b/build/windows/icon.ico new file mode 100644 index 0000000..bfa0690 Binary files /dev/null and b/build/windows/icon.ico differ diff --git a/build/windows/info.json b/build/windows/info.json new file mode 100644 index 0000000..9727946 --- /dev/null +++ b/build/windows/info.json @@ -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}}" + } + } +} \ No newline at end of file diff --git a/build/windows/wails.exe.manifest b/build/windows/wails.exe.manifest new file mode 100644 index 0000000..17e1a23 --- /dev/null +++ b/build/windows/wails.exe.manifest @@ -0,0 +1,15 @@ + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + \ No newline at end of file diff --git a/frontend/dist/index.html b/frontend/dist/index.html new file mode 100644 index 0000000..e97f806 --- /dev/null +++ b/frontend/dist/index.html @@ -0,0 +1,226 @@ + + + + + + KVMote + + + + + +
+
Posição do PC Cliente:
+ +
+ + + + + + + +
[HOST PC]
+ + + + + + +
+ +
+ + +
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ + + +
+
+ + +
+
+
+ +
+ +
+ + + + diff --git a/frontend/dist/placeholder.txt b/frontend/dist/placeholder.txt new file mode 100644 index 0000000..d6c5116 Binary files /dev/null and b/frontend/dist/placeholder.txt differ diff --git a/frontend/src/wailsjs/wailsjs/go/main/App.d.ts b/frontend/src/wailsjs/wailsjs/go/main/App.d.ts new file mode 100644 index 0000000..518439b --- /dev/null +++ b/frontend/src/wailsjs/wailsjs/go/main/App.d.ts @@ -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; + +export function Connect():Promise; + +export function Disconnect():Promise; + +export function SendCtrlAltDel():Promise; + +export function SetPosition(arg1:number):Promise; diff --git a/frontend/src/wailsjs/wailsjs/go/main/App.js b/frontend/src/wailsjs/wailsjs/go/main/App.js new file mode 100644 index 0000000..aa0160a --- /dev/null +++ b/frontend/src/wailsjs/wailsjs/go/main/App.js @@ -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); +} diff --git a/frontend/src/wailsjs/wailsjs/runtime/package.json b/frontend/src/wailsjs/wailsjs/runtime/package.json new file mode 100644 index 0000000..1e7c8a5 --- /dev/null +++ b/frontend/src/wailsjs/wailsjs/runtime/package.json @@ -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 ", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/frontend/src/wailsjs/wailsjs/runtime/runtime.d.ts b/frontend/src/wailsjs/wailsjs/runtime/runtime.d.ts new file mode 100644 index 0000000..3bbea84 --- /dev/null +++ b/frontend/src/wailsjs/wailsjs/runtime/runtime.d.ts @@ -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; + +// [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; + +// [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; + +// [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; + +// [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; + +// [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; + +// [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; + +// [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; + +// [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; + +// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext) +// Sets a text on the clipboard +export function ClipboardSetText(text: string): Promise; + +// [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; + +// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications) +// Cleans up notification resources and releases any held connections. +export function CleanupNotifications(): Promise; + +// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable) +// Checks if notifications are available on the current platform. +export function IsNotificationAvailable(): Promise; + +// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization) +// Requests notification authorization from the user (macOS only). +export function RequestNotificationAuthorization(): Promise; + +// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization) +// Checks the current notification authorization status (macOS only). +export function CheckNotificationAuthorization(): Promise; + +// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification) +// Sends a basic notification with the given options. +export function SendNotification(options: NotificationOptions): Promise; + +// [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; + +// [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; + +// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory) +// Removes a previously registered notification category. +export function RemoveNotificationCategory(categoryId: string): Promise; + +// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications) +// Removes all pending notifications from the notification center. +export function RemoveAllPendingNotifications(): Promise; + +// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification) +// Removes a specific pending notification by its identifier. +export function RemovePendingNotification(identifier: string): Promise; + +// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications) +// Removes all delivered notifications from the notification center. +export function RemoveAllDeliveredNotifications(): Promise; + +// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification) +// Removes a specific delivered notification by its identifier. +export function RemoveDeliveredNotification(identifier: string): Promise; + +// [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; \ No newline at end of file diff --git a/frontend/src/wailsjs/wailsjs/runtime/runtime.js b/frontend/src/wailsjs/wailsjs/runtime/runtime.js new file mode 100644 index 0000000..556621e --- /dev/null +++ b/frontend/src/wailsjs/wailsjs/runtime/runtime.js @@ -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); +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..97bb5fc --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fee352b --- /dev/null +++ b/go.sum @@ -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= diff --git a/internal/input/input.go b/internal/input/input.go new file mode 100644 index 0000000..5b8ddca --- /dev/null +++ b/internal/input/input.go @@ -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) +} diff --git a/internal/input/input_windows.go b/internal/input/input_windows.go new file mode 100644 index 0000000..3476f79 --- /dev/null +++ b/internal/input/input_windows.go @@ -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) +} diff --git a/internal/kvm/engine.go b/internal/kvm/engine.go new file mode 100644 index 0000000..3c44b15 --- /dev/null +++ b/internal/kvm/engine.go @@ -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 +} diff --git a/internal/transport/ble_windows.go b/internal/transport/ble_windows.go new file mode 100644 index 0000000..22587c8 --- /dev/null +++ b/internal/transport/ble_windows.go @@ -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 } diff --git a/internal/transport/transport.go b/internal/transport/transport.go new file mode 100644 index 0000000..c37f596 --- /dev/null +++ b/internal/transport/transport.go @@ -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 +} diff --git a/kvmote_debug.log b/kvmote_debug.log new file mode 100644 index 0000000..65d325b --- /dev/null +++ b/kvmote_debug.log @@ -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. diff --git a/main.go b/main.go new file mode 100644 index 0000000..7bb520a --- /dev/null +++ b/main.go @@ -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()) + } +} diff --git a/wails.json b/wails.json new file mode 100644 index 0000000..b895580 --- /dev/null +++ b/wails.json @@ -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" + } +}