Skip to content
This repository was archived by the owner on Mar 7, 2026. It is now read-only.

Commit 15372ae

Browse files
DBOYtttclaude
andcommitted
Add mouse cursor overlay to screenshots
- Create cursor-overlay.ts utility with SVG-based cursor generation - Modify screendump() to capture cursor position and overlay cursor - Cursor is rendered as black arrow with white outline for visibility - Fallback to screenshot without cursor if overlay fails This enables users to see the mouse position in screenshots sent to the API. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 86bf9e8 commit 15372ae

2 files changed

Lines changed: 111 additions & 4 deletions

File tree

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import * as sharp from 'sharp';
2+
3+
/**
4+
* Creates a cursor image as a Buffer.
5+
* The cursor is a simple arrow pointer shape.
6+
*/
7+
export async function createCursorImage(
8+
size: number = 24,
9+
color: string = '#000000',
10+
outlineColor: string = '#FFFFFF',
11+
): Promise<Buffer> {
12+
// Create a simple arrow cursor SVG
13+
const svg = `
14+
<svg width="${size}" height="${size}" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
15+
<!-- White outline for visibility on dark backgrounds -->
16+
<path d="M 2 2 L 2 20 L 7 15 L 12 22 L 15 20 L 10 13 L 17 13 Z"
17+
fill="none"
18+
stroke="${outlineColor}"
19+
stroke-width="2"
20+
stroke-linejoin="round"/>
21+
<!-- Black fill -->
22+
<path d="M 2 2 L 2 20 L 7 15 L 12 22 L 15 20 L 10 13 L 17 13 Z"
23+
fill="${color}"/>
24+
</svg>
25+
`;
26+
27+
return sharp(Buffer.from(svg)).png().toBuffer();
28+
}
29+
30+
/**
31+
* Overlays a cursor image onto a screenshot at the specified position.
32+
*
33+
* @param screenshotBuffer The screenshot image buffer
34+
* @param cursorX The x coordinate of the cursor
35+
* @param cursorY The y coordinate of the cursor
36+
* @param cursorSize The size of the cursor (default 24)
37+
* @returns A Buffer containing the screenshot with cursor overlay
38+
*/
39+
export async function overlayeCursorOnScreenshot(
40+
screenshotBuffer: Buffer,
41+
cursorX: number,
42+
cursorY: number,
43+
cursorSize: number = 24,
44+
): Promise<Buffer> {
45+
// Create the cursor image
46+
const cursorBuffer = await createCursorImage(cursorSize);
47+
48+
// Get screenshot metadata to ensure cursor stays within bounds
49+
const metadata = await sharp(screenshotBuffer).metadata();
50+
const width = metadata.width || 1920;
51+
const height = metadata.height || 1080;
52+
53+
// Ensure cursor position is within screenshot bounds
54+
const safeX = Math.max(0, Math.min(cursorX, width - 1));
55+
const safeY = Math.max(0, Math.min(cursorY, height - 1));
56+
57+
// Composite the cursor onto the screenshot
58+
return sharp(screenshotBuffer)
59+
.composite([
60+
{
61+
input: cursorBuffer,
62+
left: Math.round(safeX),
63+
top: Math.round(safeY),
64+
},
65+
])
66+
.png()
67+
.toBuffer();
68+
}

packages/bytebotd/src/nut/nut.service.ts

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from '@nut-tree-fork/nut-js';
1212
import { spawn } from 'child_process';
1313
import * as path from 'path';
14+
import { overlayeCursorOnScreenshot } from './cursor-overlay';
1415

1516
/**
1617
* Enum representing key codes supported by nut-js.
@@ -463,21 +464,59 @@ export class NutService {
463464
}
464465

465466
/**
466-
* Takes a screenshot of the screen.
467+
* Takes a screenshot of the screen with cursor overlay.
467468
*
469+
* @param includeCursor Whether to include the mouse cursor in the screenshot (default: true)
468470
* @returns A Promise that resolves with a Buffer containing the image.
469471
*/
470-
async screendump(): Promise<Buffer> {
472+
async screendump(includeCursor: boolean = true): Promise<Buffer> {
471473
const filename = `screenshot-${Date.now()}.png`;
472474
const filepath = path.join(this.screenshotDir, filename);
473475
this.logger.log(`Taking screenshot to ${filepath}`);
474476

475477
try {
478+
// Get cursor position before taking screenshot
479+
let cursorPosition: { x: number; y: number } | null = null;
480+
if (includeCursor) {
481+
try {
482+
cursorPosition = await mouse.getPosition();
483+
this.logger.log(
484+
`Cursor position: (${cursorPosition.x}, ${cursorPosition.y})`,
485+
);
486+
} catch (cursorError) {
487+
this.logger.warn(
488+
`Failed to get cursor position: ${cursorError.message}`,
489+
);
490+
}
491+
}
492+
476493
// Take screenshot
477494
await screen.capture(filename, FileType.PNG, this.screenshotDir);
478495

479-
// Read the file back and return as buffer
480-
return await import('fs').then((fs) => fs.promises.readFile(filepath));
496+
// Read the file back
497+
const screenshotBuffer = await import('fs').then((fs) =>
498+
fs.promises.readFile(filepath),
499+
);
500+
501+
// Overlay cursor if position was captured
502+
if (includeCursor && cursorPosition) {
503+
try {
504+
const withCursor = await overlayeCursorOnScreenshot(
505+
screenshotBuffer,
506+
cursorPosition.x,
507+
cursorPosition.y,
508+
);
509+
this.logger.log('Cursor overlay applied to screenshot');
510+
return withCursor;
511+
} catch (overlayError) {
512+
this.logger.warn(
513+
`Failed to overlay cursor: ${overlayError.message}. Returning screenshot without cursor.`,
514+
);
515+
return screenshotBuffer;
516+
}
517+
}
518+
519+
return screenshotBuffer;
481520
} catch (error) {
482521
this.logger.error(`Error taking screenshot: ${error.message}`);
483522
throw error;

0 commit comments

Comments
 (0)