Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions src/server/InputHandler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Button, Key, Point, keyboard, mouse } from "@nut-tree-fork/nut-js"
import { KEY_MAP } from "./KeyMap"
import { moveRelative } from "./ydotool"

export interface InputMessage {
type: "move" | "click" | "scroll" | "key" | "text" | "zoom" | "combo"
Expand Down Expand Up @@ -110,14 +111,20 @@ export class InputHandler {
Number.isFinite(msg.dx) &&
Number.isFinite(msg.dy)
) {
const currentPos = await mouse.getPosition()

await mouse.setPosition(
new Point(
Math.round(currentPos.x + msg.dx),
Math.round(currentPos.y + msg.dy),
),
)
// Attempt ydotool relative movement first
const success = await moveRelative(msg.dx, msg.dy)

// Fallback to absolute positioning if ydotool is unavailable or fails
if (!success) {
const currentPos = await mouse.getPosition()

await mouse.setPosition(
new Point(
Math.round(currentPos.x + msg.dx),
Math.round(currentPos.y + msg.dy),
),
)
}
Comment thread
imxade marked this conversation as resolved.
}
break

Expand Down
75 changes: 75 additions & 0 deletions src/server/ydotool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { execFile } from "node:child_process"
import { promisify } from "node:util"

const execFileAsync = promisify(execFile)

// Cache the availability of ydotool to avoid repeated checks
let isYdotoolAvailable: boolean | null = null
let ydotoolPath: string | null = null

// Cooldown state for failure retries
let lastFailureTime = 0
const COOLDOWN_MS = 5000 // 5 seconds before retrying

/**
* Checks if ydotool is available on the system.
*/
export async function checkYdotool(): Promise<boolean> {
if (isYdotoolAvailable !== null) {
return isYdotoolAvailable
}

const now = Date.now()
if (now - lastFailureTime < COOLDOWN_MS) {
return false // Still in cooldown
}

try {
// Suppress error output if not found, we only care if it succeeds or fails
const { stdout } = await execFileAsync("which", ["ydotool"])
ydotoolPath = stdout.trim()
Comment thread
imxade marked this conversation as resolved.
isYdotoolAvailable = !!ydotoolPath
if (isYdotoolAvailable) {
console.log(`[ydotool] Found at ${ydotoolPath}`)
}
} catch (err) {
isYdotoolAvailable = false
lastFailureTime = now
console.warn(
"[ydotool] ydotool is not available, falling back to nut.js for cursor movement.",
)
}

return isYdotoolAvailable
}

/**
* Moves the mouse cursor relatively using ydotool.
*
* @param dx X offset
* @param dy Y offset
* @returns true if successful, false otherwise
*/
export async function moveRelative(dx: number, dy: number): Promise<boolean> {
if (!(await checkYdotool()) || !ydotoolPath) {
return false
}

try {
// ydotool mousemove -x <dx> -y <dy>
await execFileAsync(ydotoolPath, [
"mousemove",
"-x",
String(dx),
"-y",
String(dy),
])
return true
} catch (err) {
console.error("[ydotool] Error executing mousemove:", err)
// Enter cooldown instead of permanently disabling
isYdotoolAvailable = null
lastFailureTime = Date.now()
return false
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}