Skip to content
Open
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
68 changes: 60 additions & 8 deletions browser/src/libs/mouse-jiggler/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import { device } from '@/libs/device';
import { MouseReportRelative } from '@/libs/mouse';

const MOUSE_JIGGLER_INTERVAL = 15_000;
const MOUSE_JIGGLER_CHECK_MS = 3_000;
const MOUSE_JIGGLER_MIN_IDLE = 30_000;
const MOUSE_JIGGLER_MAX_IDLE = 60_000;

class MouseJiggler {
private lastMoveTime: number;
private timer: number | null;
private mode: 'enable' | 'disable';
private mouseReport: MouseReportRelative;

// Figure-8 (lemniscate) pattern state
private figure8Step: number = 0;
private readonly FIGURE8_STEPS = 24;
private readonly BASE_AMPLITUDE = 35;
private isAnimating: boolean = false;

constructor() {
this.lastMoveTime = Date.now();
this.timer = null;
Expand All @@ -25,7 +33,7 @@ class MouseJiggler {
} else if (mode === 'enable' && this.timer === null) {
this.timer = setInterval(() => {
this.timeoutCallback();
}, MOUSE_JIGGLER_INTERVAL / 5);
}, MOUSE_JIGGLER_CHECK_MS);
}
}

Expand All @@ -37,19 +45,63 @@ class MouseJiggler {
}

timeoutCallback(): void {
if (Date.now() - this.lastMoveTime > MOUSE_JIGGLER_INTERVAL) {
const idleThreshold = MOUSE_JIGGLER_MIN_IDLE + Math.random() * (MOUSE_JIGGLER_MAX_IDLE - MOUSE_JIGGLER_MIN_IDLE);
if (Date.now() - this.lastMoveTime > idleThreshold && !this.isAnimating) {
this.lastMoveTime = Date.now() - 1_000;
this.sendJiggle();
}
}

/**
* Draws 1 to almost-2 figure-8 (lemniscate) loops, never completing exactly
* 2 full loops so the pointer never ends up back at its starting position.
*/
async sendJiggle(): Promise<void> {
const report1 = this.mouseReport.buildReport(10, 10, 0);
const report2 = this.mouseReport.buildReport(-10, -10, 0);
this.isAnimating = true;

const totalDuration = 5_000 + Math.random() * 5_000; // 5-10 seconds
const amp = this.BASE_AMPLITUDE + (Math.random() - 0.5) * 12;
const vertical = Math.random() > 0.5;
// Range [FIGURE8_STEPS + 1, 2 * FIGURE8_STEPS - 1] = [25, 47]
const loopSteps = this.FIGURE8_STEPS + 1 + Math.random() * (this.FIGURE8_STEPS - 2);

// --- Helper: generate variable delays totaling exactly a budget ---
const generateDelays = (steps: number, budget: number, minDelay: number): number[] => {
const extra = budget - minDelay * steps;
const weights = Array.from({ length: steps }, () => Math.random());
const sum = weights.reduce((a, b) => a + b, 0);
return weights.map((w) => minDelay + (w / sum) * extra);
};

const figure8Delays = generateDelays(loopSteps, totalDuration, 20);

// --- Phase 1: draw figure-8 loop(s) ---
for (let i = 0; i < loopSteps; i++) {
const t = (this.figure8Step / this.FIGURE8_STEPS) * 2 * Math.PI;
const tNext = ((this.figure8Step + 1) / this.FIGURE8_STEPS) * 2 * Math.PI;

const sinT = amp * Math.sin(t);
const sin2T = (amp * Math.sin(2 * t)) / 2;
const sinTNext = amp * Math.sin(tNext);
const sin2TNext = (amp * Math.sin(2 * tNext)) / 2;

const x = vertical ? sin2T : sinT;
const y = vertical ? sinT : sin2T;
const xNext = vertical ? sin2TNext : sinTNext;
const yNext = vertical ? sinTNext : sin2TNext;

const dx = Math.round(xNext - x);
const dy = Math.round(yNext - y);

const report = this.mouseReport.buildReport(dx, dy, 0);
await device.sendMouseData([0x01, ...report]);

this.figure8Step = (this.figure8Step + 1) % this.FIGURE8_STEPS;
await new Promise((resolve) => setTimeout(resolve, figure8Delays[i]));
}

await device.sendKeyboardData([0x01, ...report1]);
await device.sendKeyboardData([0x01, ...report2]);
this.isAnimating = false;
}
}

export const mouseJiggler = new MouseJiggler();
export const mouseJiggler = new MouseJiggler();
67 changes: 59 additions & 8 deletions desktop/src/renderer/src/libs/mouse-jiggler/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import { IpcEvents } from '@common/ipc-events'
import { MouseReportRelative } from '@renderer/libs/mouse'

const MOUSE_JIGGLER_INTERVAL = 15_000
const MOUSE_JIGGLER_CHECK_MS = 3_000
const MOUSE_JIGGLER_MIN_IDLE = 30_000
const MOUSE_JIGGLER_MAX_IDLE = 60_000

class MouseJiggler {
private lastMoveTime: number
private timer: NodeJS.Timeout | null
private mode: 'enable' | 'disable'
private mouseReport: MouseReportRelative

// Figure-8 (lemniscate) pattern state
private figure8Step: number = 0
private readonly FIGURE8_STEPS = 24
private readonly BASE_AMPLITUDE = 35
private isAnimating: boolean = false

constructor() {
this.lastMoveTime = Date.now()
this.timer = null
Expand All @@ -25,7 +33,7 @@ class MouseJiggler {
} else if (mode === 'enable' && this.timer === null) {
this.timer = setInterval(() => {
this.timeoutCallback()
}, MOUSE_JIGGLER_INTERVAL / 5)
}, MOUSE_JIGGLER_CHECK_MS)
}
}

Expand All @@ -37,19 +45,62 @@ class MouseJiggler {
}

timeoutCallback(): void {
if (Date.now() - this.lastMoveTime > MOUSE_JIGGLER_INTERVAL) {
const idleThreshold = MOUSE_JIGGLER_MIN_IDLE + Math.random() * (MOUSE_JIGGLER_MAX_IDLE - MOUSE_JIGGLER_MIN_IDLE)
if (Date.now() - this.lastMoveTime > idleThreshold && !this.isAnimating) {
this.lastMoveTime = Date.now() - 1_000
this.sendJiggle()
}
}

/**
* Draws 1 to almost-2 figure-8 (lemniscate) loops, never completing exactly
* 2 full loops so the pointer never ends up back at its starting position.
*/
async sendJiggle(): Promise<void> {
// Build reports directly using the report builder
const report1 = this.mouseReport.buildReport(10, 10, 0)
const report2 = this.mouseReport.buildReport(-10, -10, 0)
this.isAnimating = true

const totalDuration = 5_000 + Math.random() * 5_000 // 5-10 seconds
const amp = this.BASE_AMPLITUDE + (Math.random() - 0.5) * 12
const vertical = Math.random() > 0.5
// Range [FIGURE8_STEPS + 1, 2 * FIGURE8_STEPS - 1] = [25, 47]
const loopSteps = this.FIGURE8_STEPS + 1 + Math.random() * (this.FIGURE8_STEPS - 2)

// --- Helper: generate variable delays totaling exactly a budget ---
const generateDelays = (steps: number, budget: number, minDelay: number): number[] => {
const extra = budget - minDelay * steps
const weights = Array.from({ length: steps }, () => Math.random())
const sum = weights.reduce((a, b) => a + b, 0)
return weights.map((w) => minDelay + (w / sum) * extra)
}

const figure8Delays = generateDelays(loopSteps, totalDuration, 20)

// --- Phase 1: draw figure-8 loop(s) ---
for (let i = 0; i < loopSteps; i++) {
const t = (this.figure8Step / this.FIGURE8_STEPS) * 2 * Math.PI
const tNext = ((this.figure8Step + 1) / this.FIGURE8_STEPS) * 2 * Math.PI

const sinT = amp * Math.sin(t)
const sin2T = (amp * Math.sin(2 * t)) / 2
const sinTNext = amp * Math.sin(tNext)
const sin2TNext = (amp * Math.sin(2 * tNext)) / 2

const x = vertical ? sin2T : sinT
const y = vertical ? sinT : sin2T
const xNext = vertical ? sin2TNext : sinTNext
const yNext = vertical ? sinTNext : sin2TNext

const dx = Math.round(xNext - x)
const dy = Math.round(yNext - y)

const report = this.mouseReport.buildReport(dx, dy, 0)
await window.electron.ipcRenderer.invoke(IpcEvents.SEND_MOUSE, [0x01, ...report])

this.figure8Step = (this.figure8Step + 1) % this.FIGURE8_STEPS
await new Promise((resolve) => setTimeout(resolve, figure8Delays[i]))
}

await window.electron.ipcRenderer.invoke(IpcEvents.SEND_MOUSE, [0x01, report1])
await window.electron.ipcRenderer.invoke(IpcEvents.SEND_MOUSE, [0x01, report2])
this.isAnimating = false
}
}

Expand Down