# nut.js - Complete Site Content > This file contains the full markdown content of the nut.js documentation site. > For a summary version, see: https://nutjs.dev/llms.txt --- # Examples ## Jest Integration **Category**: Testing Frameworks **URL**: https://nutjs.dev/examples/jest-integration **Description**: Complete example of using nut.js with Jest for end-to-end desktop automation testing This example demonstrates how to integrate nut.js with Jest to create robust end-to-end tests for desktop applications. ## Project Setup First, install the required dependencies: ```bash npm install --save-dev jest @types/jest ts-jest @nut-tree/nut-js @nut-tree/bolt @nut-tree/nl-matcher ``` ## Jest Configuration Create a `jest.config.js` file: ```javascript /** @type {import('jest').Config} */ module.exports = { preset: 'ts-jest', testEnvironment: 'node', testTimeout: 30000, // Desktop automation tests may take longer setupFilesAfterEnv: ['/jest.setup.ts'], } ``` ## Setup File Create `jest.setup.ts` to configure nut.js globally: ```typescript import { keyboard, mouse, screen } from '@nut-tree/nut-js' // Configure nut.js for testing beforeAll(() => { // Set reasonable delays for test stability keyboard.config.autoDelayMs = 100 mouse.config.autoDelayMs = 100 mouse.config.mouseSpeed = 1000 }) // Optional: helper for ad-hoc screenshot debugging export async function captureDebugScreenshot(name: string) { await screen.capture(`./screenshots/${name}-${Date.now()}.png`) } ``` ## Example Test Suite Here's a complete test file (`calculator.test.ts`): ```typescript import { keyboard, screen, Key, imageResource, sleep, windowWithTitle, } from '@nut-tree/nut-js'; import {useBoltWindowFinder} from '@nut-tree/bolt'; import { useNlMatcher } from '@nut-tree/nl-matcher'; useNlMatcher(); describe('Calculator Application', () => { beforeAll(async () => { useBoltWindowFinder(); // Launch the calculator (platform-specific) if (process.platform === 'darwin') { await keyboard.pressKey(Key.LeftSuper, Key.Space) await keyboard.releaseKey(Key.LeftSuper, Key.Space) await sleep(500) await keyboard.type('Calculator') await keyboard.type(Key.Enter) } // Wait for app to launch await screen.waitFor(windowWithTitle('Calculator')); }) afterAll(async () => { // Close the calculator await keyboard.type(Key.LeftSuper, Key.Q) }) it('should perform basic addition', async () => { // Type the calculation await keyboard.type('5+3') await keyboard.type(Key.Enter) // Wait for result await sleep(500) // Verify result is displayed (using image matching) const result = await screen.waitFor(imageResource('result-8.png')) expect(result).toBeDefined() }) it('should clear the display', async () => { await keyboard.type('C') // Verify display is cleared const clearDisplay = await screen.waitFor(imageResource('clear-display.png')) expect(clearDisplay).toBeDefined() }) }) ``` ## Using Jest Matchers nut.js provides custom Jest matchers for more readable assertions: ```typescript import { screen, imageResource, jestMatchers } from '@nut-tree/nut-js' expect.extend(jestMatchers); describe('Visual Assertions', () => { it('should show the login button', async () => { await expect(screen).toShow(imageResource('login-button.png')) }) it('should show element within timeout', async () => { await expect(screen).toWaitFor(imageResource('loading-complete.png'), 5000) }) }) ``` ## Running Tests Add a script to your `package.json`: ```json { "scripts": { "test": "jest", "test:watch": "jest --watch", "test:ci": "jest --ci --reporters=default --reporters=jest-junit" } } ``` Run your tests: ```bash npm test ``` ## Tips for Stable Tests 1. **Use appropriate delays**: Desktop applications need time to render, so either use static delays via `sleep`, or use `screen.waitFor(...)` to wait for elements to appear 2. **Use image resources**: Store reference images in a dedicated folder 3. **Handle platform differences**: Use `process.platform` for OS-specific logic 4. **Set reasonable timeouts**: Default Jest timeout may be too short 5. **Clean up after tests**: Always close applications in `afterAll` --- ## Vitest Integration **Category**: Testing Frameworks **URL**: https://nutjs.dev/examples/vitest-integration **Description**: Using nut.js with Vitest for fast, modern desktop automation testing This guide shows how to use nut.js with Vitest, a blazing-fast unit test framework powered by Vite. ## Installation ```bash npm install --save-dev vitest @nut-tree/nut-js @nut-tree/nl-matcher ``` ## Vitest Configuration Create `vitest.config.ts`: ```typescript import { defineConfig } from 'vitest/config' export default defineConfig({ test: { // Desktop tests need more time testTimeout: 30000, hookTimeout: 10000, // Run tests sequentially (required for desktop automation) pool: 'forks', poolOptions: { forks: { singleFork: true, }, }, // Setup file setupFiles: ['./vitest.setup.ts'], }, }) ``` ## Setup File Create `vitest.setup.ts`: ```typescript import { beforeAll, afterEach } from 'vitest' import { keyboard, mouse, screen } from '@nut-tree/nut-js' beforeAll(() => { // Configure nut.js keyboard.config.autoDelayMs = 50 mouse.config.autoDelayMs = 50 mouse.config.mouseSpeed = 1500 }) afterEach(async (ctx) => { // Screenshot on failure if (ctx.task.result?.state === 'fail') { const name = ctx.task.name.replace(/\s+/g, '-') await screen.capture(`./screenshots/${name}-${Date.now()}.png`) } }) ``` ## Example Test Suite ```typescript import { describe, it, expect, beforeAll, afterAll } from 'vitest' import { keyboard, mouse, screen, clipboard, Key, centerOf, imageResource, Region, sleep, } from '@nut-tree/nut-js' describe('Text Editor Automation', () => { beforeAll(async () => { // Open a text editor if (process.platform === 'darwin') { await keyboard.pressKey(Key.LeftSuper, Key.Space) await keyboard.releaseKey(Key.LeftSuper, Key.Space) await sleep(300) await keyboard.type('TextEdit') await keyboard.pressKey(Key.Enter) await keyboard.releaseKey(Key.Enter) await sleep(1000) } }) afterAll(async () => { // Close without saving await keyboard.pressKey(Key.LeftSuper, Key.Q) await keyboard.releaseKey(Key.LeftSuper, Key.Q) await sleep(300) // Don't save dialog await keyboard.pressKey(Key.LeftSuper, Key.D) await keyboard.releaseKey(Key.LeftSuper, Key.D) }) it('should type text into the editor', async () => { const testText = 'Hello from Vitest!' await keyboard.type(testText) // Verify by selecting all and checking await keyboard.type(Key.LeftSuper, Key.A) await sleep(200) // Copy to clipboard await keyboard.type(Key.LeftSuper, Key.C) const clipboardText = await clipboard.paste() expect(clipboardText).toContain(testText) }) it('should perform find and replace', async () => { // Clear and type new text await keyboard.type(Key.LeftSuper, Key.A) await keyboard.type('foo bar foo baz foo') // Open find and replace await keyboard.type(Key.LeftSuper, Key.LeftAlt, Key.F) await sleep(500) // Type search term await keyboard.type('foo') await keyboard.type(Key.Tab) await keyboard.type('replaced') // Replace all await keyboard.type(Key.LeftSuper, Key.LeftAlt, Key.G) await sleep(300) // Close find dialog await keyboard.type(Key.Escape) }) }) ``` ## Custom Matchers for Vitest Create custom matchers in your setup file: ```typescript import { expect } from 'vitest' import { screen, imageResource, vitestMatchers } from '@nut-tree/nut-js' import { useNlMatcher } from '@nut-tree/nl-matcher' useNlMatcher() expect.extend(vitestMatchers) ``` Usage: ```typescript it('should display the welcome screen', async () => { await expect(screen).toShow(imageResource('welcome-dialog.png')) }) ``` ## Running Tests ```bash # Run all tests npx vitest run # Watch mode npx vitest # With UI npx vitest --ui ``` ## CI Configuration Example GitHub Actions workflow: ```yaml name: Desktop Tests on: [push, pull_request] jobs: test: runs-on: macos-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' - run: npm ci - run: npx vitest run env: CI: true - uses: actions/upload-artifact@v4 if: failure() with: name: screenshots path: screenshots/ ``` --- ## Playwright Bridge Example **Category**: Integrations **URL**: https://nutjs.dev/examples/playwright-bridge **Description**: Visual automation with the Playwright Bridge plugin This example demonstrates using `@nut-tree/playwright-bridge` to automate a browser visually. The bridge redirects nut.js operations into the browser viewport, enabling image-based automation within web pages. ## Setup ```typescript import { chromium, selectors } from "playwright"; import { locateByPosition, useContext } from "@nut-tree/playwright-bridge"; import { centerOf, ConsoleLogLevel, imageResource, keyboard, mouse, screen, sleep, useConsoleLogger, useDefaultMouseProvider, useDefaultWindowProvider, windowWithTitle, } from "@nut-tree/nut-js"; import { useNlMatcher } from "@nut-tree/nl-matcher"; useNlMatcher(); // Register position selector before creating any pages await selectors.register("atPosition", locateByPosition); ``` > **Important:** Custom selectors like `atPosition` must be registered with `selectors.register()` **before** creating any `Page` instances. Playwright locks the selector registry once a page is created, so registering after that point will fail. ```typescript useConsoleLogger({ logLevel: ConsoleLogLevel.DEBUG }); const browser = await chromium.launch({ headless: false }); const ctx = await browser.newContext(); screen.config.resourceDirectory = "./images"; keyboard.config.autoDelayMs = 10; mouse.config.mouseSpeed = 3000; screen.config.autoHighlight = true; // Redirect nut.js operations to the browser context useContext({ context: ctx }); ``` ## Finding and Clicking Elements Use `screen.find()` and `screen.waitFor()` to locate elements by their appearance within the viewport: ```typescript const page = await ctx.newPage(); await page.goto("https://example.com"); // Find an element visually with match validation const target = await centerOf( screen.find(imageResource("target.png"), { confidence: 0.9, providerData: { validateMatches: true }, }) ); // Use the position to click via Playwright's locator await page.locator(`atPosition=${target}`).click(); ``` The `atPosition` selector converts a nut.js `Point` into a Playwright locator. `Point.toString()` outputs coordinates in the expected format, so you can interpolate it directly. ## Interacting with Located Elements Once you have a Playwright locator, you can use standard Playwright methods: ```typescript const searchBox = await screen.find(imageResource("search.png"), { confidence: 0.79, providerData: { validateMatches: true }, }); const searchBoxPosition = await centerOf(searchBox); // Get a Playwright locator at the found position const box = await page.locator(`atPosition=${searchBoxPosition}`); await box.click(); await box.fill("nut.js"); await box.press("Enter"); ``` ## Waiting for Visual Changes Wait for elements to appear after an action: ```typescript const result = await screen.waitFor( imageResource("result.png"), 7000, 1000, { confidence: 0.9, providerData: { validateMatches: true }, } ); ``` ## Capturing Screenshots of Matched Regions Combine a nut.js match region with Playwright's screenshot API: ```typescript const result = await screen.waitFor(imageResource("result.png"), 7000, 1000); // Use the nut.js Region to clip a Playwright screenshot await page.screenshot({ path: "result.png", clip: { x: result.left, y: result.top, width: result.width, height: result.height, }, }); ``` ## Switching Between Browser and Desktop A powerful pattern is switching between the browser context and the desktop. This lets you manipulate the actual browser window (move, resize) and then continue with in-browser automation: ```typescript // Start in browser context useContext({ context: ctx }); const page = await ctx.newPage(); await page.goto("https://example.com"); const pageTitle = await page.title(); // Switch to desktop providers to manipulate the browser window useDefaultWindowProvider(); useDefaultMouseProvider(); const browserWindow = await screen.find(windowWithTitle(pageTitle)); await browserWindow.move({ x: 100, y: 500 }); await browserWindow.resize({ width: 1000, height: 800 }); // Switch back to browser context for in-viewport operations useContext({ context: ctx }); const button = await screen.find(imageResource("button.png")); // ...continue with in-browser automation ``` ## Using with @playwright/test For test suites, use `playwrightMatchers` with `expect`: ```typescript import { test, expect } from "@playwright/test"; import { screen, imageResource } from "@nut-tree/nut-js"; import { useNlMatcher } from "@nut-tree/nl-matcher"; useNlMatcher(); import { playwrightMatchers, usePage } from "@nut-tree/playwright-bridge"; expect.extend(playwrightMatchers); test("should display the welcome screen", async ({ page }) => { usePage({ page }); await page.goto("https://example.com"); await expect(screen).toShow(imageResource("welcome-dialog.png")); await expect(screen).toWaitFor(imageResource("loaded.png"), 5000, 500); }); ``` ## Cleanup Always close the context and browser when done: ```typescript await ctx.close(); await browser.close(); ``` ## Key Points - **`useContext()`** redirects all nut.js operations to a Playwright browser context - **`usePage()`** targets a specific page (useful in `@playwright/test` with `trackPageChanges`) - **`locateByPosition`** must be registered with `selectors.register()` before creating pages - **`useDefaultWindowProvider()` / `useDefaultMouseProvider()`** switch back to desktop for window manipulation - **`useNlMatcher()`** must be called to activate the image matching provider - Use `providerData: { validateMatches: true }` for higher accuracy at the cost of speed For complete API documentation, see the [Playwright Bridge docs](/plugins/playwright-bridge). --- ## Electron App Testing **Category**: Integrations **URL**: https://nutjs.dev/examples/electron-testing **Description**: Complete guide to testing Electron applications with nut.js and @playwright/test Electron apps combine web technologies with native desktop capabilities, making them uniquely challenging to test. This guide shows how to leverage both Playwright's web automation and nut.js's desktop automation for comprehensive Electron testing. ## Why Combine Playwright and nut.js? Electron apps have two distinct layers: | Layer | Tool | Capabilities | |-------|------|--------------| | **Web Content** | Playwright | DOM queries, clicks, form filling, network interception | | **Native Desktop** | nut.js | Window management, system dialogs, native menus, screenshots | Using both tools together gives you complete test coverage. ## Project Setup ### Installation ```bash npm install --save-dev @playwright/test @nut-tree/nut-js ``` ### Playwright Configuration Create `playwright.config.ts`: ```typescript import { defineConfig } from '@playwright/test' export default defineConfig({ testDir: './tests', timeout: 30000, workers: 1, }) ``` > **Important:** Set `workers: 1` — desktop automation requires sequential execution since tests interact with the same screen and window manager. ## Basic Test Structure Here's the foundational pattern for Electron + nut.js testing: ```typescript import { _electron as electron, ElectronApplication, Page, JSHandle } from "playwright"; import { test, expect } from "@playwright/test"; import { sleep, getActiveWindow, screen, getWindows } from "@nut-tree/nut-js"; import { fileURLToPath } from "url"; import { dirname } from "path"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); let app: ElectronApplication; let page: Page; let windowHandle: JSHandle; const APP_TIMEOUT = 10000; test.beforeEach(async () => { // Launch the Electron app app = await electron.launch({ args: ["main.cjs"], cwd: __dirname, env: { ...process.env, NODE_ENV: "development", }, }); // Get the first window page = await app.firstWindow({ timeout: APP_TIMEOUT }); windowHandle = await app.browserWindow(page); // Wait for the app to be ready await page.waitForLoadState("domcontentloaded"); // Ensure window is focused (critical for nut.js) await windowHandle.evaluate((win: any) => { win.minimize(); win.restore(); win.focus(); }); }); test.afterEach(async () => { if (app) { await app.close(); } }); ``` ## Window Management Tests ### Listing Application Windows ```typescript test.describe("getWindows", () => { test("should list our started application window", async () => { const openWindows = await getWindows(); const windowNames = await Promise.all(openWindows.map((wnd) => wnd.title)); expect(windowNames).toContain("My Electron App"); }); }); ``` ### Getting the Active Window ```typescript test.describe("getActiveWindow", () => { test("should return our started application window", async () => { const foregroundWindow = await getActiveWindow(); const windowTitle = await foregroundWindow.title; expect(windowTitle).toBe("My Electron App"); }); test("should determine correct window position", async () => { const foregroundWindow = await getActiveWindow(); const region = await foregroundWindow.region; expect(region.left).toBeGreaterThanOrEqual(0); expect(region.top).toBeGreaterThanOrEqual(0); expect(region.width).toBeGreaterThan(0); expect(region.height).toBeGreaterThan(0); }); }); ``` ### Moving and Resizing Windows ```typescript test("should move the window to a new position", async () => { const newX = 142; const newY = 425; const foregroundWindow = await getActiveWindow(); await foregroundWindow.move({ x: newX, y: newY }); await sleep(1000); // Wait for window manager const region = await foregroundWindow.region; expect(region.left).toBe(newX); expect(region.top).toBe(newY); }); test("should resize the window", async () => { const newWidth = 800; const newHeight = 600; const foregroundWindow = await getActiveWindow(); await foregroundWindow.resize({ width: newWidth, height: newHeight }); await sleep(1000); const region = await foregroundWindow.region; expect(region.width).toBe(newWidth); expect(region.height).toBe(newHeight); }); ``` ### Handling Screen Boundaries ```typescript test.describe("window regions", () => { test("should handle window positioned beyond left edge", async () => { const newLeft = -40; const originalWidth = 400; const foregroundWindow = await getActiveWindow(); await foregroundWindow.move({ x: newLeft, y: 100 }); await sleep(1000); const region = await foregroundWindow.region; // Window is cropped to screen boundary expect(region.left).toBe(0); expect(region.width).toBe(originalWidth + newLeft); }); test("should handle window positioned beyond right edge", async () => { const screenWidth = await screen.width(); const delta = 40; const newLeft = screenWidth - delta; const foregroundWindow = await getActiveWindow(); await foregroundWindow.move({ x: newLeft, y: 100 }); await sleep(1000); const region = await foregroundWindow.region; expect(region.left).toBe(newLeft); expect(region.width).toBe(delta); }); }); ``` ## Hybrid Web + Desktop Testing The real power comes from combining Playwright's DOM access with nut.js's desktop capabilities. ### Testing Web Content + Native Dialogs ```typescript import { keyboard, Key, mouse, centerOf, imageResource } from "@nut-tree/nut-js"; test("should save file using native dialog", async () => { // Use Playwright to trigger the save action await page.click('[data-testid="save-button"]'); // Wait for native dialog to appear await sleep(1000); // Handle native file dialog with nut.js if (process.platform === 'darwin') { // macOS: Type filename and press Enter await keyboard.type("test-document.txt"); await sleep(300); await keyboard.pressKey(Key.Enter); await keyboard.releaseKey(Key.Enter); } else if (process.platform === 'win32') { // Windows: Similar approach await keyboard.type("test-document.txt"); await sleep(300); await keyboard.pressKey(Key.Enter); await keyboard.releaseKey(Key.Enter); } // Verify save completed via web content await expect(page.locator('[data-testid="save-status"]')).toHaveText("Saved"); }); ``` ### Testing Native Menus ```typescript test("should open preferences via native menu", async () => { if (process.platform === 'darwin') { // macOS: App Menu > Preferences await keyboard.pressKey(Key.LeftSuper, Key.Comma); await keyboard.releaseKey(Key.LeftSuper, Key.Comma); } else { // Windows/Linux: File > Preferences or Edit > Preferences await keyboard.pressKey(Key.LeftAlt); await keyboard.releaseKey(Key.LeftAlt); await sleep(200); await keyboard.type("f"); // File menu await sleep(200); await keyboard.type("p"); // Preferences } await sleep(500); // Verify preferences window opened (via Playwright or nut.js) const windows = await getWindows(); const titles = await Promise.all(windows.map((w) => w.title)); expect(titles.some(t => t.includes("Preferences"))).toBe(true); }); ``` ### Testing Drag and Drop ```typescript import { mouse, centerOf, straightTo, Button } from "@nut-tree/nut-js"; test("should drag item from sidebar to main area", async () => { // Get element positions via Playwright const sourceElement = await page.locator('[data-testid="sidebar-item"]').boundingBox(); const targetElement = await page.locator('[data-testid="drop-zone"]').boundingBox(); if (!sourceElement || !targetElement) { throw new Error("Elements not found"); } // Perform native drag with nut.js const sourceCenter = { x: sourceElement.x + sourceElement.width / 2, y: sourceElement.y + sourceElement.height / 2, }; const targetCenter = { x: targetElement.x + targetElement.width / 2, y: targetElement.y + targetElement.height / 2, }; await mouse.move(straightTo(sourceCenter)); await mouse.pressButton(Button.LEFT); await sleep(100); await mouse.move(straightTo(targetCenter)); await sleep(100); await mouse.releaseButton(Button.LEFT); // Verify drop completed await expect(page.locator('[data-testid="drop-zone"]')).toContainText("Item dropped"); }); ``` ## Image-Based Verification Use nut.js screen capture for visual verification: ```typescript import { screen, Region, imageResource } from "@nut-tree/nut-js"; test("should display the correct toolbar icons", async () => { // Get window region const foregroundWindow = await getActiveWindow(); const windowRegion = await foregroundWindow.region; // Define toolbar region const toolbarRegion = new Region( windowRegion.left, windowRegion.top, windowRegion.width, 60 ); // Find expected icon in toolbar const saveIcon = await screen.find(imageResource("save-icon.png"), { searchRegion: toolbarRegion, }); expect(saveIcon).toBeDefined(); }); test("should take screenshot on failure", async () => { try { await expect(page.locator('[data-testid="missing-element"]')).toBeVisible(); } catch (error) { // Capture desktop screenshot for debugging await screen.capture(`./screenshots/failure-${Date.now()}.png`); throw error; } }); ``` ## Testing Multi-Window Scenarios ```typescript test("should open and manage multiple windows", async () => { // Open new window via web content await page.click('[data-testid="new-window-button"]'); await sleep(1000); // Verify new window exists const windows = await getWindows(); const appWindows = []; for (const win of windows) { const title = await win.title; if (title.includes("My Electron App")) { appWindows.push(win); } } expect(appWindows.length).toBe(2); // Focus the second window if (appWindows[1]) { await appWindows[1].focus(); await sleep(500); const activeWindow = await getActiveWindow(); const activeTitle = await activeWindow.title; expect(activeTitle).toBe(await appWindows[1].title); } }); ``` ## System Tray Testing ```typescript test("should show tray menu when clicking system tray icon", async () => { // Minimize app to tray await windowHandle.evaluate((win: any) => win.hide()); await sleep(500); // Platform-specific tray interaction if (process.platform === 'darwin') { // macOS: Tray is in menu bar - find and click the icon const trayIcon = await screen.find(imageResource("tray-icon-mac.png")); await mouse.move(straightTo(centerOf(trayIcon))); await mouse.click(Button.LEFT); } else if (process.platform === 'win32') { // Windows: Tray is in system tray area const trayIcon = await screen.find(imageResource("tray-icon-win.png")); await mouse.move(straightTo(centerOf(trayIcon))); await mouse.click(Button.RIGHT); // Right-click for context menu } await sleep(300); // Verify tray menu appeared const menuItem = await screen.find(imageResource("tray-menu-open.png")); expect(menuItem).toBeDefined(); }); ``` ## CI/CD Considerations ### GitHub Actions ```yaml name: E2E Tests on: [push, pull_request] jobs: test: strategy: matrix: os: [macos-latest, windows-latest, ubuntu-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' - name: Install dependencies run: npm ci - name: Install Playwright run: npx playwright install --with-deps # Linux requires virtual display - name: Setup virtual display (Linux) if: runner.os == 'Linux' run: | sudo apt-get install -y xvfb Xvfb :99 -screen 0 1920x1080x24 & echo "DISPLAY=:99" >> $GITHUB_ENV - name: Run E2E tests run: npx playwright test - name: Upload screenshots on failure if: failure() uses: actions/upload-artifact@v4 with: name: screenshots-${{ matrix.os }} path: screenshots/ ``` ## Best Practices ### Window Focus Always ensure the window is focused before nut.js operations: ```typescript // Force focus before each test test.beforeEach(async () => { await windowHandle.evaluate((win: any) => { win.minimize(); win.restore(); win.focus(); }); await sleep(300); }); ``` ### Timing Desktop operations need time to complete: ```typescript // Wait for window manager operations await foregroundWindow.move({ x: 100, y: 100 }); await sleep(1000); // Wait for animation // Wait for native dialogs await page.click('[data-testid="open-dialog"]'); await sleep(1000); // Dialog animation ``` ### Error Handling Capture screenshots on failure for debugging: ```typescript test.afterEach(async ({}, testInfo) => { if (testInfo.status !== testInfo.expectedStatus) { const name = testInfo.title.replace(/\s+/g, '-'); await screen.capture(`./screenshots/${name}-${Date.now()}.png`); } }); ``` ### Platform-Specific Logic ```typescript function getPlatformShortcut(key: string): Key[] { const modifier = process.platform === 'darwin' ? Key.LeftSuper : Key.LeftControl; const keyMap: Record = { 's': Key.S, 'c': Key.C, 'v': Key.V, }; return [modifier, keyMap[key] || Key.A]; } ``` ## Complete Example Here's a full test file combining all concepts: ```typescript import { _electron as electron, ElectronApplication, Page, JSHandle } from "playwright"; import { test, expect } from "@playwright/test"; import { sleep, getActiveWindow, screen, getWindows, keyboard, mouse, Key, Button, centerOf, straightTo, imageResource, } from "@nut-tree/nut-js"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); let app: ElectronApplication; let page: Page; let windowHandle: JSHandle; test.beforeEach(async () => { app = await electron.launch({ args: [join(__dirname, "main.cjs")], env: { ...process.env, NODE_ENV: "test" }, }); page = await app.firstWindow({ timeout: 10000 }); windowHandle = await app.browserWindow(page); await page.waitForLoadState("domcontentloaded"); await windowHandle.evaluate((win: any) => { win.minimize(); win.restore(); win.focus(); }); await sleep(500); }); test.afterEach(async ({}, testInfo) => { // Screenshot on failure if (testInfo.status !== testInfo.expectedStatus) { await screen.capture(`./screenshots/fail-${Date.now()}.png`); } if (app) { await app.close(); } }); test.describe("Document Editor", () => { test("should create, edit, and save a document", async () => { // 1. Create new document (web content) await page.click('[data-testid="new-doc-button"]'); await expect(page.locator('[data-testid="editor"]')).toBeVisible(); // 2. Type content using nut.js keyboard await page.click('[data-testid="editor"]'); await keyboard.type("Hello, this is a test document."); // 3. Save using keyboard shortcut const modifier = process.platform === 'darwin' ? Key.LeftSuper : Key.LeftControl; await keyboard.pressKey(modifier, Key.S); await keyboard.releaseKey(modifier, Key.S); // 4. Handle native save dialog await sleep(1000); await keyboard.type("test-document"); await keyboard.pressKey(Key.Enter); await keyboard.releaseKey(Key.Enter); // 5. Verify save completed await sleep(500); await expect(page.locator('[data-testid="save-status"]')).toHaveText("Saved"); }); test("should verify window appears in nut.js window list", async () => { const windows = await getWindows(); const titles = await Promise.all(windows.map(w => w.title)); expect(titles).toContain("Document Editor"); }); }); ``` --- ## RDP Automation **Category**: Integrations **URL**: https://nutjs.dev/examples/rdp-automation **Description**: Automate a remote desktop session via RDP with nut.js This example demonstrates using `@nut-tree/plugin-rdp` to automate a remote Windows machine via RDP. The plugin redirects nut.js operations to the remote session, enabling image-based automation, keyboard input, mouse control, and clipboard access over the connection. ## Setup ```typescript import "@nut-tree/nl-matcher"; import { centerOf, clipboard, imageResource, Key, keyboard, mouse, screen, sleep, straightTo, } from "@nut-tree/nut-js"; import { connect } from "@nut-tree/plugin-rdp"; ``` Load credentials from environment variables: ```typescript import { config } from "dotenv"; config(); ``` ## Connecting to the Remote Machine ```typescript screen.config.confidence = 0.87; screen.config.resourceDirectory = "./resources"; const remote = await connect({ hostname: process.env.REMOTE_HOSTNAME, port: parseInt(process.env.REMOTE_PORT), username: process.env.REMOTE_USERNAME, password: process.env.REMOTE_PASSWORD, ignoreCertificate: true, }); console.log(`Connected: ${remote.isConnected()}`); ``` > **Important:** Store credentials in environment variables or a `.env` file — never hardcode them. The `ignoreCertificate` option should only be used in development or trusted environments. ## Redirecting Providers After connecting, call `useRemote()` to redirect all nut.js operations to the remote machine. You can also activate providers individually: ```typescript // Redirect all at once remote.useRemote(); // Or activate individually remote.useRemoteScreen(); remote.useRemoteKeyboard(); remote.useRemoteMouse(); remote.useRemoteClipboard(); ``` ## Automating the Remote Desktop Once providers are redirected, use the standard nut.js APIs. All operations happen on the remote machine: ```typescript // Allow the remote desktop to fully render await sleep(3000); // Wait for the Windows start button to appear await screen.waitFor(imageResource("start.png"), 10000, 1000, { providerData: { validateMatches: true }, }); // Open Notepad via the Start menu await keyboard.type(Key.LeftWin); await sleep(1000); await keyboard.type("notepad"); await keyboard.type(Key.Enter); // Type some text await keyboard.type("Hello world! nut.js ❤️ you!"); ``` ## Image-Based Interaction Use `screen.waitFor()` and `mouse.move()` to find and click elements visually: ```typescript const target = await screen.waitFor( imageResource("helo.png"), 10000, 1000, { providerData: { validateMatches: true } }, ); await mouse.move(straightTo(centerOf(target))); ``` ## Clipboard Operations With `useRemoteClipboard()`, the clipboard API operates on the remote machine: ```typescript // Select all and copy on the remote machine await keyboard.type(Key.LeftControl, Key.A); await keyboard.type(Key.LeftControl, Key.C); // Read from the remote clipboard const clip = await clipboard.getContent(); console.log(`Clipboard: ${clip}`); // Write to the remote clipboard and paste await clipboard.setContent("We ❤️ you too, nut.js!"); await keyboard.type(Key.LeftControl, Key.A); await keyboard.type(Key.LeftControl, Key.V); ``` ## Error Handling and Cleanup Always disconnect in a `finally` block. Combine with screen recording to capture failures: ```typescript import { useScreenRecorder } from "@nut-tree/plugin-screenrecording"; useScreenRecorder(); const remote = await connect({ /* ... */ }); remote.useRemote(); await screen.startRecording({ fps: 30, bufferSeconds: 5 }); try { // ... automation steps ... } catch (e) { console.error(e); await screen.stopRecording({ outputPath: "./error.mp4" }); } finally { remote.disconnect(); } await screen.discardRecording(); ``` ## Key Points - **`connect()`** establishes the RDP session and returns provider registration functions - **`useRemote()`** redirects all nut.js operations (screen, keyboard, mouse, clipboard) to the remote machine - Individual `useRemoteScreen()`, `useRemoteKeyboard()`, `useRemoteMouse()`, `useRemoteClipboard()` allow selective redirection - **`isConnected()`** checks whether the RDP session is still active - **`disconnect()`** closes the connection — always call it in a `finally` block - Works with other plugins like `@nut-tree/nl-matcher` (image matching) and `@nut-tree/plugin-screenrecording` (recording) - Store credentials in environment variables, not in source code For complete API documentation, see the [RDP plugin docs](/plugins/rdp). --- ## Selenium Bridge Example **Category**: Integrations **URL**: https://nutjs.dev/examples/selenium-bridge **Description**: Visual testing with the Selenium Bridge plugin This example demonstrates using `@nut-tree/selenium-bridge` to automate a login flow by finding elements visually. The bridge redirects nut.js operations into the browser viewport controlled by Selenium WebDriver. ## Basic Setup ```typescript import { Builder, Browser } from "selenium-webdriver"; import { screen, mouse, keyboard, imageResource, centerOf, straightTo, Button, } from "@nut-tree/nut-js"; import { useNlMatcher } from "@nut-tree/nl-matcher"; useNlMatcher(); import { useSelenium } from "@nut-tree/selenium-bridge"; async function main() { const driver = await new Builder() .forBrowser(Browser.CHROME) .build(); try { // Initialize the bridge useSelenium({ driver }); await driver.get("https://example.com/login"); ``` ## Visual Login Flow ```typescript // Find username field by image and type const usernameField = await screen.find(imageResource("username-field.png")); await mouse.move(straightTo(centerOf(usernameField))); await mouse.click(Button.LEFT); await keyboard.type("testuser"); // Find password field const passwordField = await screen.find(imageResource("password-field.png")); await mouse.move(straightTo(centerOf(passwordField))); await mouse.click(Button.LEFT); await keyboard.type("secretpassword"); // Find and click login button const loginBtn = await screen.find(imageResource("login-button.png")); await mouse.move(straightTo(centerOf(loginBtn))); await mouse.click(Button.LEFT); // Wait for dashboard to load (timeout: 10000ms, interval: 1000ms) await screen.waitFor(imageResource("dashboard-header.png"), 10000, 1000); console.log("Login successful!"); } finally { await driver.quit(); } } ``` ## Converting Regions to WebElements Use `elementAt()` to get a Selenium WebElement from a visual match. Pass the region directly: ```typescript import { Builder, Browser, WebElement } from "selenium-webdriver"; import { screen, imageResource } from "@nut-tree/nut-js"; import { useNlMatcher } from "@nut-tree/nl-matcher"; useNlMatcher(); import { useSelenium, elementAt } from "@nut-tree/selenium-bridge"; const driver = await new Builder() .forBrowser(Browser.CHROME) .build(); useSelenium({ driver }); await driver.get("https://example.com/products"); // Find element visually const productCard = await screen.find(imageResource("featured-product.png")); // Convert region to WebElement (pass the region directly) const productElement: WebElement = await elementAt(productCard); // Use standard Selenium methods const productId = await productElement.getAttribute("data-product-id"); await productElement.click(); ``` ## Jest Integration ```typescript import { Builder, Browser, WebDriver } from "selenium-webdriver"; import { screen, mouse, imageResource, centerOf, straightTo, Button } from "@nut-tree/nut-js"; import { useNlMatcher } from "@nut-tree/nl-matcher"; useNlMatcher(); import { useSelenium } from "@nut-tree/selenium-bridge"; describe("Visual E2E Tests", () => { let driver: WebDriver; beforeAll(async () => { driver = await new Builder().forBrowser(Browser.CHROME).build(); useSelenium({ driver }); }); afterAll(async () => { await driver.quit(); }); test("should display hero section", async () => { await driver.get("https://example.com"); const hero = await screen.find(imageResource("hero-section.png")); expect(hero).toBeDefined(); expect(hero.width).toBeGreaterThan(500); }); test("should open menu on click", async () => { await driver.get("https://example.com"); const menuBtn = await screen.find(imageResource("menu-button.png")); await mouse.move(straightTo(centerOf(menuBtn))); await mouse.click(Button.LEFT); const navMenu = await screen.waitFor(imageResource("nav-menu.png"), 3000, 500); expect(navMenu).toBeDefined(); }); }); ``` ## Key Points - **`useSelenium()`** redirects all nut.js operations into the browser viewport - **`elementAt(region)`** converts a screen region to a Selenium WebElement (pass the region directly) - **`useNlMatcher()`** must be called to activate the image matching provider - Works with any Selenium-supported browser (Chrome, Firefox, Edge, Safari) - The bridge follows the active window when switching tabs For complete API documentation, see the [Selenium Bridge docs](/plugins/selenium-bridge). --- ## Element Inspector **Category**: Advanced Usage **URL**: https://nutjs.dev/examples/element-inspector **Description**: Using the nut.js Element Inspector for native UI element discovery and automation The Element Inspector lets you automate native applications by interacting with their UI element tree rather than matching pixels. This approach is more reliable across themes, resolutions, and minor UI changes. ## When to Use Element Inspector - **Native desktop apps** where you need to interact with menus, buttons, text fields - **Dynamic content** that changes appearance but keeps the same structure - **Accessibility testing** to verify proper element labeling - **Complex workflows** involving nested UI hierarchies For web automation, consider the [Playwright Bridge](/plugins/playwright-bridge) or [Selenium Bridge](/plugins/selenium-bridge) instead. ## Installation ```bash npm i @nut-tree/element-inspector ``` Supported on **Windows**, **macOS**, and **Linux**. ## Prerequisites The target application must have accessibility features enabled. For example, Visual Studio Code requires: ```json "editor.accessibilitySupport": "on" ``` ## Exploring an Application's UI Tree Before automating, you'll want to see what elements are available: ```js import { useConsoleLogger, ConsoleLogLevel, screen, windowWithTitle } from "@nut-tree/nut-js"; import {useBolt} from "@nut-tree/bolt"; import { useElementInspector } from "@nut-tree/element-inspector"; useElementInspector(); import {elements} from "@nut-tree/element-inspector/win"; // For macOS: // import {elements} from "@nut-tree/element-inspector/macos"; // For Linux: // import {elements} from "@nut-tree/element-inspector/linux"; useConsoleLogger({logLevel: ConsoleLogLevel.DEBUG}); useBolt(); const app = await screen.find(windowWithTitle(/Visual Studio Code/)); await app.focus(); // Limit depth to avoid huge output (default is 100 levels) const tree = await app.getElements(5); console.log(JSON.stringify(tree, null, 2)); ``` This outputs a tree structure showing each element's `id`, `role`, `title`, and `type`: ```json { "type": "Group", "children": [ { "role": "toolbar", "type": "ToolBar", "children": [ { "role": "button", "title": "Go Back (Alt+LeftArrow)", "type": "Button" } ] } ] } ``` ## Finding and Clicking an Element Once you know what elements exist, you can find and interact with them: ```js import { screen, windowWithTitle, mouse, Button, straightTo, centerOf } from "@nut-tree/nut-js"; import {useBolt} from "@nut-tree/bolt"; import { useElementInspector } from "@nut-tree/element-inspector"; useElementInspector(); import {elements} from "@nut-tree/element-inspector/win"; useBolt(); const app = await screen.find(windowWithTitle(/Visual Studio Code/)); await app.focus(); // Find the File menu by title const fileMenu = await app.find(elements.menuItem({title: "File"})); if (fileMenu.region != null) { await screen.highlight(fileMenu.region); await mouse.move(straightTo(centerOf(fileMenu.region))); await mouse.click(Button.LEFT); } ``` ## Finding Elements by Any Property Use `windowElementDescribedBy` when you don't know the element type but know other properties: ```js import {screen, windowWithTitle} from "@nut-tree/nut-js"; import {useBolt} from "@nut-tree/bolt"; import { useElementInspector } from "@nut-tree/element-inspector"; useElementInspector(); import {windowElementDescribedBy} from "@nut-tree/element-inspector/win"; useBolt(); const app = await screen.find(windowWithTitle(/Visual Studio Code/)); await app.focus(); // Find any element with title "File", regardless of type const element = await app.find(windowElementDescribedBy({title: "File"})); ``` You can query by any combination of these properties: ```ts interface WindowElementDescription { id?: string | RegExp; type?: string; title?: string | RegExp; value?: string | RegExp; selectedText?: string | RegExp; role?: string; } ``` ## Finding All Matching Elements Get all elements of a type, for example all menu items: ```js import {screen, windowWithTitle} from "@nut-tree/nut-js"; import {useBolt} from "@nut-tree/bolt"; import { useElementInspector } from "@nut-tree/element-inspector"; useElementInspector(); import {elements} from "@nut-tree/element-inspector/win"; useBolt(); const app = await screen.find(windowWithTitle(/Visual Studio Code/)); const menuItems = await app.findAll(elements.menuItem({})); // Get all titles const titles = await Promise.all(menuItems.map(item => item.title)); console.log("Menu items:", titles); ``` ## Waiting for Elements to Appear Wait for UI elements that appear after some action: ```js import {screen, windowWithTitle} from "@nut-tree/nut-js"; import {useBolt} from "@nut-tree/bolt"; import { useElementInspector } from "@nut-tree/element-inspector"; useElementInspector(); import {elements} from "@nut-tree/element-inspector/win"; useBolt(); const app = await screen.find(windowWithTitle(/Visual Studio Code/)); // Click something that opens a menu, then wait for it const menu = await app.waitFor(elements.menu({})); console.log("Menu appeared"); ``` ## Navigating Nested Hierarchies Use the `in:` property to find elements within specific parent elements: ```js import {screen, windowWithTitle, mouse, Button, straightTo, centerOf} from "@nut-tree/nut-js"; import { useElementInspector } from "@nut-tree/element-inspector"; useElementInspector(); import {elements} from "@nut-tree/element-inspector/win"; import {useBoltWindowFinder} from "@nut-tree/bolt"; useBoltWindowFinder(); const explorer = await screen.find(windowWithTitle("This PC")); await explorer.focus(); // Find "boot" folder inside a DVD drive, inside Desktop const bootFolder = await explorer.find(elements.treeItem({ title: "boot", in: elements.treeItem({ title: /DVD.*/, in: elements.treeItem({ title: "Desktop" }) }) })); await mouse.move(straightTo(centerOf(bootFolder.region))); await mouse.click(Button.LEFT); // Find files within a specific group const files = await explorer.findAll(elements.listItem({ title: /.*boot.*/, in: elements.group({ title: "Files Currently on the Disc" }) })); ``` ## Ancestor and Descendant Relations Use `descendantOf` and `ancestorOf` for flexible tree traversal that doesn't require the element to be a direct child or parent: ```js import {screen, windowWithTitle} from "@nut-tree/nut-js"; import {useBolt} from "@nut-tree/bolt"; import { useElementInspector } from "@nut-tree/element-inspector"; useElementInspector(); import {elements} from "@nut-tree/element-inspector/win"; useBolt(); const app = await screen.find(windowWithTitle(/My App/)); // Find a button anywhere inside a dialog (not just direct children) const okButton = await app.find(elements.button({ title: "OK", descendantOf: elements.dialog({ title: "Confirm" }), })); // Find the group that contains a specific button const container = await app.find(elements.group({ ancestorOf: elements.button({ title: "Submit" }), })); ``` ## Sibling Relations Target elements relative to their siblings using `after`, `before`, and `siblingOf`: ```js import {screen, windowWithTitle} from "@nut-tree/nut-js"; import {useBolt} from "@nut-tree/bolt"; import { useElementInspector } from "@nut-tree/element-inspector"; useElementInspector(); import {elements} from "@nut-tree/element-inspector/win"; useBolt(); const app = await screen.find(windowWithTitle(/My App/)); // Find the text field that appears after a "Username" label const usernameField = await app.find(elements.textField({ after: elements.staticText({ title: "Username" }), })); // Find the button that appears before "Cancel" const submitBtn = await app.find(elements.button({ before: elements.button({ title: "Cancel" }), })); // Find any button that is a sibling of a specific checkbox const relatedBtn = await app.find(elements.button({ siblingOf: elements.checkbox({ title: "Remember me" }), })); ``` ## Position Modifiers Use `nthChild`, `firstChild`, and `lastChild` to select elements by their position among siblings: ```js import {screen, windowWithTitle} from "@nut-tree/nut-js"; import {useBolt} from "@nut-tree/bolt"; import { useElementInspector } from "@nut-tree/element-inspector"; useElementInspector(); import {elements} from "@nut-tree/element-inspector/win"; useBolt(); const app = await screen.find(windowWithTitle(/My App/)); // Get the third tab (0-indexed) const thirdTab = await app.find(elements.tab({ nthChild: 2, in: elements.tabGroup({}), })); // Get the first item in a list const firstItem = await app.find(elements.listItem({ firstChild: true, in: elements.list({}), })); // Get the last menu item const lastItem = await app.find(elements.menuItem({ lastChild: true, in: elements.menu({}), })); ``` ## Excluding Elements Use `notMatching` to filter out unwanted matches: ```js import {screen, windowWithTitle} from "@nut-tree/nut-js"; import {useBolt} from "@nut-tree/bolt"; import { useElementInspector } from "@nut-tree/element-inspector"; useElementInspector(); import {elements} from "@nut-tree/element-inspector/win"; useBolt(); const app = await screen.find(windowWithTitle(/My App/)); // Find all enabled buttons (exclude disabled ones) const enabledButtons = await app.findAll(elements.button({ notMatching: elements.button({ isEnabled: false }), })); ``` ## Tips - **Start with `getElements()`** to understand the app's structure before writing automation - **Use shallow depth** (e.g., `getElements(3)`) for faster exploration - **Regex patterns** work for `title`, `id`, and `value` properties - **Element regions can be null** if the element isn't currently visible - always check before interacting - **Different apps expose different elements** - what's available depends on how the app implements accessibility For the complete API, see the [Element Inspector plugin page](https://nutjs.dev/plugins/element-inspector). --- ## Image Matching **Category**: Advanced Usage **URL**: https://nutjs.dev/examples/image-matching **Description**: Master image-based automation with template matching and OCR Image matching is a core feature of nut.js that allows you to find and interact with UI elements based on their visual appearance. ## Basic Image Matching ```typescript import { screen, mouse, straightTo, centerOf, imageResource, } from '@nut-tree/nut-js' import { useNlMatcher } from "@nut-tree/nl-matcher"; useNlMatcher(); async function clickOnImage() { // Find an image on screen const result = await screen.find(imageResource('button.png')) // Move to center and click await mouse.move(straightTo(centerOf(result))) await mouse.click() } ``` ## Configuring Confidence Levels ```typescript import { screen, imageResource } from '@nut-tree/nut-js' import { useNlMatcher } from "@nut-tree/nl-matcher"; useNlMatcher(); // Set global confidence (0.0 - 1.0) screen.config.confidence = 0.9 async function findWithConfidence() { // Or per-search confidence const result = await screen.find(imageResource('icon.png'), { confidence: 0.85, }) return result } ``` ## Waiting for Images ```typescript import { screen, imageResource } from '@nut-tree/nut-js' import { useNlMatcher } from "@nut-tree/nl-matcher"; useNlMatcher(); async function waitForImage() { // Wait up to 10 seconds for image to appear (timeout, interval) const result = await screen.waitFor(imageResource('loading-complete.png'), 10000, 500) console.log('Image appeared at:', result) } ``` ## Finding Multiple Matches ```typescript import { screen, mouse, straightTo, centerOf, imageResource } from '@nut-tree/nut-js' import { useNlMatcher } from "@nut-tree/nl-matcher"; useNlMatcher(); async function findAllMatches() { // Find all instances of an image const matches = await screen.findAll(imageResource('list-item.png')) console.log(`Found ${matches.length} matches`) // Click each one for (const match of matches) { await mouse.move(straightTo(centerOf(match))) await mouse.click() } } ``` ## Search Regions Limit search to a specific area for better performance: ```typescript import { screen, Region, imageResource } from '@nut-tree/nut-js' import { useNlMatcher } from "@nut-tree/nl-matcher"; useNlMatcher(); async function searchInRegion() { // Define search region (left, top, width, height) const searchArea = new Region(100, 100, 800, 600) // Search only in that region const result = await screen.find(imageResource('target.png'), { searchRegion: searchArea, }) return result } ``` ## OCR Text Matching Use OCR to find text on screen: ```typescript import { screen, mouse, straightTo, centerOf, singleWord } from '@nut-tree/nut-js' import { useOcrPlugin, configure, LanguageModelType, Language } from '@nut-tree/plugin-ocr' // Configure OCR useOcrPlugin(); configure({ dataPath: "./ocr-data", languageModelType: LanguageModelType.BEST }); async function findByText() { // Find text on screen using singleWord const result = await screen.find(singleWord('Submit'), { providerData: { lang: [Language.English] } }) await mouse.move(straightTo(centerOf(result))) await mouse.click() } async function findTextCaseInsensitive() { // Find text with case-insensitive matching const result = await screen.find(singleWord('order'), { providerData: { lang: [Language.English], partialMatch: true, caseSensitive: false } }) console.log('Found order at:', result) } ``` ## Capturing Screenshots ```typescript import { screen, Region } from '@nut-tree/nut-js' async function captureScreenshots() { // Full screen capture await screen.capture('full-screen.png') // Capture specific region const region = new Region(0, 0, 400, 300) await screen.captureRegion('partial.png', region) // Get image data without saving const imageData = await screen.grab() console.log(`Screen size: ${imageData.width}x${imageData.height}`) } ``` ## Creating Reference Images Best practices for creating reliable reference images: ```typescript import { screen, Region, mouse } from '@nut-tree/nut-js' async function createReferenceImage() { // Position your UI element, then capture it console.log('Position the element and press Enter...') // Capture a region around the mouse cursor const pos = await mouse.getPosition() const region = new Region( pos.x - 50, pos.y - 25, 100, 50 ) await screen.captureRegion('reference-images/my-button.png', region) console.log('Reference image saved!') } ``` ## Handling Scale and Resolution ```typescript import { screen, imageResource } from '@nut-tree/nut-js' // For Retina/HiDPI displays screen.config.scaleMultiplier = 2 async function findOnHiDPI() { // nut.js handles scaling automatically const result = await screen.find(imageResource('button@2x.png')) return result } ``` ## Color Matching Find pixels by color: ```typescript import { screen, Point, RGBA, pixelWithColor } from '@nut-tree/nut-js' async function findByColor() { // Define the target color const red: RGBA = { R: 255, G: 0, B: 0, A: 255 } // Find first pixel of that color const result = await screen.find(pixelWithColor(red)) console.log(`Found red pixel at: ${result.left}, ${result.top}`) // Find all pixels of that color const allRed = await screen.findAll(pixelWithColor(red)) console.log(`Found ${allRed.length} red pixels`) return result } async function readColorAtPosition() { // Get the color at a specific position const color = await screen.colorAt(new Point(100, 200)) console.log(`Color: R=${color.R}, G=${color.G}, B=${color.B}, A=${color.A}`) } ``` ## Performance Optimization ```typescript import { screen, Region, imageResource } from '@nut-tree/nut-js' import { useNlMatcher } from "@nut-tree/nl-matcher"; useNlMatcher(); // Cache images for repeated searches const cachedButton = imageResource('button.png') async function optimizedSearch() { // 1. Use search regions const toolbar = new Region(0, 0, 1920, 100) // 2. Lower confidence for faster matching (when appropriate) screen.config.confidence = 0.8 // 3. Use cached image resources const result = await screen.find(cachedButton, { searchRegion: toolbar, }) return result } ``` ## Debugging Image Matching ```typescript import { screen, imageResource } from '@nut-tree/nut-js' import { useNlMatcher } from "@nut-tree/nl-matcher"; useNlMatcher(); async function debugImageMatching() { try { const result = await screen.find(imageResource('target.png')) console.log('Found at:', result) } catch (error) { // Capture current screen for comparison await screen.capture('debug-current-screen.png') console.log('Image not found. Screen captured for debugging.') // Check if the reference image exists // Compare with captured screen manually } } ``` ## Cross-Platform Images Store platform-specific reference images: ```typescript import { screen, imageResource } from '@nut-tree/nut-js' import { useNlMatcher } from "@nut-tree/nl-matcher"; useNlMatcher(); import path from 'path' function platformImage(name: string) { const platform = process.platform // 'darwin', 'win32', 'linux' return imageResource(path.join('images', platform, name)) } async function crossPlatformClick() { const button = await screen.find(platformImage('submit-button.png')) // ... click the button } ``` --- ## Text Search (OCR) **Category**: Advanced Usage **URL**: https://nutjs.dev/examples/text-ocr **Description**: Find and interact with text on screen using optical character recognition The OCR plugin enables you to find and read text directly from the screen. This is useful when automating applications where text is rendered dynamically, or when maintaining reference images isn't practical. ## When to Use OCR - **Dynamic labels** that change based on data (prices, usernames, timestamps) - **Localized applications** where button text varies by language - **Data extraction** from screens that don't expose their content programmatically - **Verification** that expected text appears after an action For static UI elements, [image matching](/examples/image-matching) is often faster and more reliable. ## Installation ```bash npm i @nut-tree/plugin-ocr ``` ## Quick Start ```typescript import {screen, singleWord, mouse, straightTo, centerOf} from "@nut-tree/nut-js"; import {configure, LanguageModelType, preloadLanguages, Language} from "@nut-tree/plugin-ocr"; // Configure once at startup configure({ dataPath: "./ocr-data", // Where to store language models languageModelType: LanguageModelType.BEST }); await preloadLanguages([Language.English]); // Find and click text const button = await screen.find(singleWord("Submit")); await mouse.move(straightTo(centerOf(button))); ``` ## Finding Text on Screen The `singleWord` function (from `@nut-tree/nut-js`) creates a text query. Configure matching behavior via `providerData`: ```typescript import {screen, mouse, singleWord, straightTo, centerOf} from "@nut-tree/nut-js"; import {useOcrPlugin, configure, Language, LanguageModelType, preloadLanguages} from "@nut-tree/plugin-ocr"; useOcrPlugin(); configure({ dataPath: "/path/to/store/language/models", languageModelType: LanguageModelType.BEST }); await preloadLanguages([Language.English, Language.German]); screen.config.ocrConfidence = 0.8; // Minimum confidence threshold screen.config.autoHighlight = true; // Highlight matches for debugging const location = await screen.find(singleWord("WebStorm"), { providerData: { lang: [Language.English, Language.German], partialMatch: false, // Require exact word match caseSensitive: false // Ignore case } }); await mouse.move(straightTo(centerOf(location))); ``` ### Provider Options | Option | Type | Description | |--------|------|-------------| | `lang` | `Language[]` | Languages to use for recognition | | `partialMatch` | `boolean` | Allow matching part of a word | | `caseSensitive` | `boolean` | Whether to match case exactly | ## Reading Text from a Region Extract text from a specific area of the screen: ```typescript import {getActiveWindow, screen} from "@nut-tree/nut-js"; import {useOcrPlugin, configure, LanguageModelType, TextSplit, preloadLanguages, Language} from "@nut-tree/plugin-ocr"; useOcrPlugin(); configure({ dataPath: "/path/to/store/language/models", languageModelType: LanguageModelType.BEST }); await preloadLanguages([Language.English, Language.German]); const activeWindow = await getActiveWindow(); // Read text, split by lines const text = await screen.read({ searchRegion: activeWindow.region, split: TextSplit.LINE }); console.log(text); ``` ### TextSplit Options | Value | Description | |-------|-------------| | `TextSplit.SYMBOL` | Individual characters | | `TextSplit.WORD` | Words | | `TextSplit.LINE` | Lines | | `TextSplit.PARAGRAPH` | Paragraphs | | `TextSplit.BLOCK` | Text blocks | | `TextSplit.NONE` | All text as one string | ## Configuration Reference ### OCR Plugin Configuration ```typescript configure({ dataPath: string, // Directory for language model files languageModelType: LanguageModelType // FAST, DEFAULT, or BEST }); ``` ### Language Model Types | Type | Speed | Accuracy | Use Case | |------|-------|----------|----------| | `FAST` | Fastest | Lower | Quick checks, known fonts | | `DEFAULT` | Balanced | Good | General use | | `BEST` | Slower | Highest | Complex layouts, small text | ## Tips - **Preload languages** before searching to avoid delays on first use - **Use search regions** to limit where OCR looks - faster and more accurate - **Lower confidence** (`screen.config.ocrConfidence`) if text isn't being found, but expect more false positives - **Enable `autoHighlight`** during development to see what's being matched - **`partialMatch: true`** helps when text might include extra characters or whitespace ## Supported Languages The plugin supports 100+ languages. Common ones include: `Language.English`, `Language.German`, `Language.French`, `Language.Spanish`, `Language.Italian`, `Language.Portuguese`, `Language.Dutch`, `Language.Polish`, `Language.Russian`, `Language.Japanese`, `Language.ChineseSimplified`, `Language.ChineseTraditional`, `Language.Korean`, `Language.Arabic`, `Language.Hindi` See the full list in the [OCR plugin documentation](https://nutjs.dev/plugins/ocr). --- ## Color Search **Category**: Advanced Usage **URL**: https://nutjs.dev/examples/color-search **Description**: Find and interact with UI elements based on pixel colors Color search allows you to find pixels or detect UI states based on color. This is useful for status indicators, validation states, and visual feedback detection. ## Reading Pixel Colors ```typescript import { screen, Point } from "@nut-tree/nut-js"; async function getPixelColor(x: number, y: number) { const color = await screen.colorAt(new Point(x, y)); console.log(`Pixel at (${x}, ${y}):`); console.log(` Red: ${color.R}`); console.log(` Green: ${color.G}`); console.log(` Blue: ${color.B}`); console.log(` Alpha: ${color.A}`); return color; } ``` ## Detect Status Indicator Check the color of a status light to determine state: ```typescript import { screen, Point } from "@nut-tree/nut-js"; type Status = "success" | "error" | "warning" | "unknown"; async function getStatusIndicator(x: number, y: number): Promise { const color = await screen.colorAt(new Point(x, y)); // Green = success (high G, low R and B) if (color.G > 200 && color.R < 100 && color.B < 100) { return "success"; } // Red = error (high R, low G and B) if (color.R > 200 && color.G < 100 && color.B < 100) { return "error"; } // Yellow/Orange = warning (high R and G, low B) if (color.R > 200 && color.G > 150 && color.B < 100) { return "warning"; } return "unknown"; } // Usage const status = await getStatusIndicator(50, 100); console.log(`Current status: ${status}`); if (status === "error") { console.error("Error detected! Taking screenshot..."); await screen.capture("error-state.png"); } ``` ## Wait for Color Change Poll until a pixel changes to an expected color: ```typescript import { screen, Point } from "@nut-tree/nut-js"; async function waitForGreen( x: number, y: number, timeoutMs: number = 10000 ): Promise { const startTime = Date.now(); while (Date.now() - startTime < timeoutMs) { const color = await screen.colorAt(new Point(x, y)); if (color.G > 200 && color.R < 100 && color.B < 100) { console.log("Indicator turned green!"); return true; } // Wait 100ms before checking again await new Promise(r => setTimeout(r, 100)); } throw new Error("Timed out waiting for green indicator"); } // Wait for build status to turn green await waitForGreen(50, 100, 30000); ``` ## Generic Color Wait Function ```typescript import { screen, Point, RGBA } from "@nut-tree/nut-js"; interface ColorCondition { R?: { min?: number; max?: number }; G?: { min?: number; max?: number }; B?: { min?: number; max?: number }; } function matchesCondition(color: RGBA, condition: ColorCondition): boolean { if (condition.R) { if (condition.R.min !== undefined && color.R < condition.R.min) return false; if (condition.R.max !== undefined && color.R > condition.R.max) return false; } if (condition.G) { if (condition.G.min !== undefined && color.G < condition.G.min) return false; if (condition.G.max !== undefined && color.G > condition.G.max) return false; } if (condition.B) { if (condition.B.min !== undefined && color.B < condition.B.min) return false; if (condition.B.max !== undefined && color.B > condition.B.max) return false; } return true; } async function waitForColor( x: number, y: number, condition: ColorCondition, timeoutMs: number = 10000 ): Promise { const startTime = Date.now(); while (Date.now() - startTime < timeoutMs) { const color = await screen.colorAt(new Point(x, y)); if (matchesCondition(color, condition)) { return color; } await new Promise(r => setTimeout(r, 100)); } throw new Error("Timed out waiting for color condition"); } // Usage: wait for any green-ish color await waitForColor(50, 100, { G: { min: 150 }, R: { max: 100 }, B: { max: 100 }, }); ``` ## Sample Multiple Points Read colors from multiple positions for gradient verification or multi-indicator checks: ```typescript import { screen, Point, RGBA } from "@nut-tree/nut-js"; interface ColorSample { x: number; y: number; color: RGBA; } async function sampleGradient( startX: number, y: number, count: number, spacing: number ): Promise { const samples: ColorSample[] = []; for (let i = 0; i < count; i++) { const x = startX + (i * spacing); const color = await screen.colorAt(new Point(x, y)); samples.push({ x, y, color }); } return samples; } // Sample 10 points across a gradient const gradient = await sampleGradient(100, 200, 10, 20); gradient.forEach(({ x, color }) => { console.log(`x=${x}: RGB(${color.R}, ${color.G}, ${color.B})`); }); ``` ## Check Multiple Status Indicators ```typescript import { screen, Point } from "@nut-tree/nut-js"; interface StatusIndicator { name: string; x: number; y: number; } type StatusResult = "success" | "error" | "warning" | "unknown"; async function checkAllIndicators( indicators: StatusIndicator[] ): Promise> { const results = new Map(); for (const indicator of indicators) { const color = await screen.colorAt(new Point(indicator.x, indicator.y)); let status: StatusResult = "unknown"; if (color.G > 200 && color.R < 100) { status = "success"; } else if (color.R > 200 && color.G < 100) { status = "error"; } else if (color.R > 200 && color.G > 150) { status = "warning"; } results.set(indicator.name, status); } return results; } // Usage const indicators: StatusIndicator[] = [ { name: "Database", x: 50, y: 100 }, { name: "API", x: 50, y: 130 }, { name: "Cache", x: 50, y: 160 }, ]; const statuses = await checkAllIndicators(indicators); statuses.forEach((status, name) => { console.log(`${name}: ${status}`); }); ``` ## Finding Pixels by Color ```typescript import { screen, RGBA } from "@nut-tree/nut-js"; import { pixelWithColor } from "@nut-tree/nut-js"; async function findRedPixel() { const targetColor: RGBA = { R: 255, G: 0, B: 0, A: 255 }; // Find first pixel of that color const location = await screen.find(pixelWithColor(targetColor)); console.log(`Found red pixel at: ${location.left}, ${location.top}`); return location; } async function findAllRedPixels() { const targetColor: RGBA = { R: 255, G: 0, B: 0, A: 255 }; // Find all pixels of that color const allRed = await screen.findAll(pixelWithColor(targetColor)); console.log(`Found ${allRed.length} red pixels`); return allRed; } ``` ## Color Comparison with Tolerance ```typescript import { screen, Point, RGBA } from "@nut-tree/nut-js"; function colorsMatch(c1: RGBA, c2: RGBA, tolerance: number = 10): boolean { return ( Math.abs(c1.R - c2.R) <= tolerance && Math.abs(c1.G - c2.G) <= tolerance && Math.abs(c1.B - c2.B) <= tolerance ); } async function findColorWithTolerance( targetColor: RGBA, tolerance: number = 10 ) { const width = await screen.width(); const height = await screen.height(); // Sample grid of points for (let y = 0; y < height; y += 10) { for (let x = 0; x < width; x += 10) { const color = await screen.colorAt(new Point(x, y)); if (colorsMatch(color, targetColor, tolerance)) { return { x, y, color }; } } } return null; } ``` ## Detect Theme (Light/Dark Mode) ```typescript import { screen, Point, Region } from "@nut-tree/nut-js"; async function detectTheme(): Promise<"light" | "dark"> { // Sample the background color from a known location const bgColor = await screen.colorAt(new Point(100, 100)); // Calculate luminance const luminance = (0.299 * bgColor.R + 0.587 * bgColor.G + 0.114 * bgColor.B); return luminance > 128 ? "light" : "dark"; } async function getThemeAwareCoordinates() { const theme = await detectTheme(); // Return different coordinates based on theme // (useful when UI elements shift between themes) if (theme === "dark") { return { buttonX: 150, buttonY: 200 }; } else { return { buttonX: 155, buttonY: 205 }; } } ``` ## Progress Bar Detection ```typescript import { screen, Point, RGBA } from "@nut-tree/nut-js"; async function getProgressBarPercentage( startX: number, endX: number, y: number, filledColor: RGBA, tolerance: number = 20 ): Promise { const totalWidth = endX - startX; let filledWidth = 0; for (let x = startX; x <= endX; x++) { const color = await screen.colorAt(new Point(x, y)); const matches = ( Math.abs(color.R - filledColor.R) <= tolerance && Math.abs(color.G - filledColor.G) <= tolerance && Math.abs(color.B - filledColor.B) <= tolerance ); if (matches) { filledWidth = x - startX; } else { break; // Progress bar is contiguous } } return Math.round((filledWidth / totalWidth) * 100); } // Usage: check a green progress bar const progress = await getProgressBarPercentage( 100, 500, 300, { R: 76, G: 175, B: 80, A: 255 } // Material green ); console.log(`Progress: ${progress}%`); ``` ## Best Practices ### Use Regions for Performance ```typescript import { screen, Point, Region } from "@nut-tree/nut-js"; // Don't search the entire screen // Instead, limit to where you expect the element const statusBarRegion = new Region(0, 0, 200, 50); async function checkStatusInRegion() { // Sample a few points within the known region const color = await screen.colorAt(new Point( statusBarRegion.left + 25, statusBarRegion.top + 25 )); return color; } ``` ### Account for Anti-Aliasing ```typescript import { screen, Point, RGBA } from "@nut-tree/nut-js"; // Sample multiple nearby pixels and average them async function getAverageColor(centerX: number, centerY: number): Promise { const samples: RGBA[] = []; for (let dx = -1; dx <= 1; dx++) { for (let dy = -1; dy <= 1; dy++) { const color = await screen.colorAt(new Point(centerX + dx, centerY + dy)); samples.push(color); } } return { R: Math.round(samples.reduce((sum, c) => sum + c.R, 0) / samples.length), G: Math.round(samples.reduce((sum, c) => sum + c.G, 0) / samples.length), B: Math.round(samples.reduce((sum, c) => sum + c.B, 0) / samples.length), A: 255 }; } ``` --- ## Legacy Application Automation **Category**: Advanced Usage **URL**: https://nutjs.dev/examples/legacy-app-automation **Description**: Automate legacy desktop applications that lack modern APIs using nut.js Legacy applications—those Win32 apps, Java Swing UIs, and enterprise systems built decades ago—often lack modern automation APIs. They're the backbone of many organizations but notoriously difficult to test. nut.js provides the tools to automate these applications reliably. ## The Legacy Challenge Legacy applications typically have these automation obstacles: | Challenge | Impact | nut.js Solution | |-----------|--------|-----------------| | No DOM or web technologies | Can't use Playwright/Selenium | Image and OCR search | | Limited accessibility APIs | Element inspection unreliable | Visual automation | | Custom UI frameworks | Standard locators don't work | Template matching | | Non-standard controls | No consistent identifiers | Color and region-based detection | ## Strategy Overview The key to legacy app automation is **visual reliability**: 1. **Launch and detect** - Start the app and verify it's ready 2. **Navigate visually** - Use image matching to find UI elements 3. **Interact precisely** - Click, type, and wait for responses 4. **Verify results** - Check screen content for expected outcomes ## Basic Setup ```typescript import { screen, keyboard, mouse, Key, Button, Region, sleep, centerOf, straightTo, imageResource, getWindows, getActiveWindow, } from "@nut-tree/nut-js"; import { exec } from "child_process"; import { promisify } from "util"; const execAsync = promisify(exec); // Configure for legacy app speed keyboard.config.autoDelayMs = 50; mouse.config.autoDelayMs = 50; mouse.config.mouseSpeed = 1000; // Set resource directory for reference images screen.config.resourceDirectory = "./reference-images"; // Lower confidence for anti-aliased legacy UIs screen.config.confidence = 0.9; ``` ## Launching Legacy Applications ### Windows Applications ```typescript async function launchWindowsApp(exePath: string, windowTitle: string): Promise { // Launch the application await execAsync(`start "" "${exePath}"`); // Wait for window to appear const maxAttempts = 30; for (let i = 0; i < maxAttempts; i++) { await sleep(1000); const windows = await getWindows(); for (const win of windows) { const title = await win.title; if (title.includes(windowTitle)) { // Focus the window await win.focus(); await sleep(500); return; } } } throw new Error(`Application window "${windowTitle}" not found after ${maxAttempts} seconds`); } // Usage await launchWindowsApp("C:\\Program Files\\LegacyApp\\app.exe", "Legacy Application"); ``` ### macOS Applications ```typescript async function launchMacApp(appPath: string, windowTitle: string): Promise { await execAsync(`open "${appPath}"`); const maxAttempts = 30; for (let i = 0; i < maxAttempts; i++) { await sleep(1000); const windows = await getWindows(); for (const win of windows) { const title = await win.title; if (title.includes(windowTitle)) { await win.focus(); await sleep(500); return; } } } throw new Error(`Application window "${windowTitle}" not found`); } // Usage await launchMacApp("/Applications/LegacyApp.app", "Legacy Application"); ``` ### Java Applications ```typescript async function launchJavaApp(jarPath: string, mainClass: string, windowTitle: string): Promise { // Launch Java app with specific JVM options const javaCmd = `java -jar "${jarPath}"`; // For main class: java -cp "${jarPath}" ${mainClass} exec(javaCmd); // Don't await - it blocks const maxAttempts = 60; // Java apps can be slow to start for (let i = 0; i < maxAttempts; i++) { await sleep(1000); const windows = await getWindows(); for (const win of windows) { const title = await win.title; if (title.includes(windowTitle)) { await win.focus(); await sleep(1000); // Extra time for Java UI return; } } } throw new Error(`Java application window not found`); } ``` ## Finding and Clicking UI Elements ### Image-Based Button Clicks ```typescript async function clickButton(buttonImageName: string, timeout: number = 10000): Promise { const button = await screen.waitFor(imageResource(buttonImageName), timeout); await mouse.move(straightTo(centerOf(button))); await mouse.click(Button.LEFT); await sleep(300); // Wait for UI response } // Usage await clickButton("login-button.png"); await clickButton("submit-form.png"); await clickButton("menu-file.png"); ``` ### Region-Limited Searches For faster, more reliable searches: ```typescript async function clickButtonInRegion( buttonImage: string, region: Region ): Promise { const button = await screen.find(imageResource(buttonImage), { searchRegion: region, }); await mouse.move(straightTo(centerOf(button))); await mouse.click(Button.LEFT); } // Define known regions const TOOLBAR_REGION = new Region(0, 0, 1920, 80); const SIDEBAR_REGION = new Region(0, 80, 250, 800); const MAIN_CONTENT_REGION = new Region(250, 80, 1670, 800); // Search only in toolbar await clickButtonInRegion("save-icon.png", TOOLBAR_REGION); ``` ### Handling Multiple Similar Elements ```typescript async function clickNthMatch(buttonImage: string, index: number): Promise { const matches = await screen.findAll(imageResource(buttonImage)); if (matches.length <= index) { throw new Error(`Only found ${matches.length} matches, expected at least ${index + 1}`); } // Sort by position (top-to-bottom, left-to-right) matches.sort((a, b) => { if (Math.abs(a.top - b.top) < 10) { return a.left - b.left; } return a.top - b.top; }); await mouse.move(straightTo(centerOf(matches[index]))); await mouse.click(Button.LEFT); } // Click the third checkbox await clickNthMatch("checkbox-unchecked.png", 2); ``` ## Text Input in Legacy Forms ### Simple Text Fields ```typescript async function fillTextField(fieldImage: string, value: string): Promise { // Find and click the field const field = await screen.find(imageResource(fieldImage)); await mouse.move(straightTo(centerOf(field))); await mouse.click(Button.LEFT); await sleep(100); // Clear existing content await keyboard.pressKey(Key.LeftControl, Key.A); await keyboard.releaseKey(Key.LeftControl, Key.A); await sleep(50); // Type new value await keyboard.type(value); } await fillTextField("username-field.png", "admin"); await fillTextField("password-field.png", "secretpassword"); ``` ### Tab-Based Form Navigation Many legacy forms use Tab key navigation: ```typescript interface FormField { value: string; isPassword?: boolean; } async function fillFormWithTabs(startFieldImage: string, fields: FormField[]): Promise { // Click the first field const firstField = await screen.find(imageResource(startFieldImage)); await mouse.move(straightTo(centerOf(firstField))); await mouse.click(Button.LEFT); await sleep(100); for (const field of fields) { // Clear and type await keyboard.pressKey(Key.LeftControl, Key.A); await keyboard.releaseKey(Key.LeftControl, Key.A); await keyboard.type(field.value); // Tab to next field await keyboard.pressKey(Key.Tab); await keyboard.releaseKey(Key.Tab); await sleep(100); } } // Fill a registration form await fillFormWithTabs("first-name-field.png", [ { value: "John" }, { value: "Doe" }, { value: "john.doe@example.com" }, { value: "555-123-4567" }, ]); ``` ### Handling Special Characters ```typescript async function typeWithShift(text: string): Promise { for (const char of text) { if (char === char.toUpperCase() && char !== char.toLowerCase()) { // Uppercase letter await keyboard.pressKey(Key.LeftShift); await keyboard.type(char.toLowerCase()); await keyboard.releaseKey(Key.LeftShift); } else if ("!@#$%^&*()_+{}|:\"<>?".includes(char)) { // Shift symbols await keyboard.pressKey(Key.LeftShift); await keyboard.type(char); await keyboard.releaseKey(Key.LeftShift); } else { await keyboard.type(char); } await sleep(20); } } ``` ## Handling Menus and Dropdowns ### Cascading Menus ```typescript async function navigateMenu(menuPath: string[]): Promise { for (let i = 0; i < menuPath.length; i++) { const menuItem = await screen.waitFor(imageResource(menuPath[i]), 5000); await mouse.move(straightTo(centerOf(menuItem))); if (i === menuPath.length - 1) { // Last item - click it await mouse.click(Button.LEFT); } else { // Intermediate item - hover to open submenu await sleep(500); } } } // Navigate: File > Export > PDF await navigateMenu([ "menu-file.png", "menu-export.png", "menu-export-pdf.png", ]); ``` ### Dropdown Selection ```typescript async function selectDropdownOption( dropdownImage: string, optionImage: string ): Promise { // Click dropdown to open const dropdown = await screen.find(imageResource(dropdownImage)); await mouse.move(straightTo(centerOf(dropdown))); await mouse.click(Button.LEFT); await sleep(500); // Find and click option const option = await screen.waitFor(imageResource(optionImage), 5000); await mouse.move(straightTo(centerOf(option))); await mouse.click(Button.LEFT); await sleep(300); } await selectDropdownOption("country-dropdown.png", "country-usa.png"); ``` ### Keyboard-Based Dropdown For dropdowns that respond to keyboard: ```typescript async function selectDropdownByTyping( dropdownImage: string, searchText: string ): Promise { const dropdown = await screen.find(imageResource(dropdownImage)); await mouse.move(straightTo(centerOf(dropdown))); await mouse.click(Button.LEFT); await sleep(300); // Type to filter/select await keyboard.type(searchText); await sleep(200); await keyboard.pressKey(Key.Enter); await keyboard.releaseKey(Key.Enter); } // Type "Cal" to select "California" from state dropdown await selectDropdownByTyping("state-dropdown.png", "Cal"); ``` ## Handling Dialogs ### Waiting for Modal Dialogs ```typescript async function waitForDialog(dialogImage: string, timeout: number = 10000): Promise { return await screen.waitFor(imageResource(dialogImage), timeout); } async function dismissDialog(okButtonImage: string): Promise { const okButton = await screen.find(imageResource(okButtonImage)); await mouse.move(straightTo(centerOf(okButton))); await mouse.click(Button.LEFT); await sleep(500); } // Wait for confirmation dialog and click OK await waitForDialog("confirmation-dialog.png"); await dismissDialog("dialog-ok-button.png"); ``` ### System Dialogs (Open/Save) ```typescript async function handleSaveDialog(filename: string, folder?: string): Promise { await sleep(1000); // Wait for dialog if (folder) { // Navigate to folder using keyboard shortcut if (process.platform === 'darwin') { await keyboard.pressKey(Key.LeftSuper, Key.LeftShift, Key.G); await keyboard.releaseKey(Key.LeftSuper, Key.LeftShift, Key.G); } else { // Windows: Click in address bar await keyboard.pressKey(Key.LeftAlt, Key.D); await keyboard.releaseKey(Key.LeftAlt, Key.D); } await sleep(300); await keyboard.type(folder); await keyboard.pressKey(Key.Enter); await keyboard.releaseKey(Key.Enter); await sleep(500); } // Type filename await keyboard.type(filename); await sleep(200); // Save await keyboard.pressKey(Key.Enter); await keyboard.releaseKey(Key.Enter); } async function handleOpenDialog(filePath: string): Promise { await sleep(1000); // Use Go to folder on macOS or address bar on Windows if (process.platform === 'darwin') { await keyboard.pressKey(Key.LeftSuper, Key.LeftShift, Key.G); await keyboard.releaseKey(Key.LeftSuper, Key.LeftShift, Key.G); await sleep(300); } else { await keyboard.pressKey(Key.LeftAlt, Key.D); await keyboard.releaseKey(Key.LeftAlt, Key.D); await sleep(300); } await keyboard.type(filePath); await keyboard.pressKey(Key.Enter); await keyboard.releaseKey(Key.Enter); } ``` ## OCR for Dynamic Content When text changes but you need to find it: ```typescript import { screen, mouse, straightTo, centerOf, Button, Region, textLine } from "@nut-tree/nut-js"; import { configure, Language, LanguageModelType, preloadLanguages } from "@nut-tree/plugin-ocr"; configure({ dataPath: "./ocr-data", languageModelType: LanguageModelType.BEST }); await preloadLanguages([Language.English]); async function clickTextButton(buttonText: string): Promise { const button = await screen.find(textLine(buttonText), { providerData: { lang: [Language.English], partialMatch: true, caseSensitive: false } }); await mouse.move(straightTo(centerOf(button))); await mouse.click(Button.LEFT); } async function verifyScreenContains(expectedText: string): Promise { try { await screen.find(textLine(expectedText), { providerData: { lang: [Language.English], partialMatch: true, caseSensitive: false } }); return true; } catch { return false; } } async function readFieldValue(fieldRegion: Region): Promise { return await screen.read(fieldRegion); } // Usage await clickTextButton("Submit Order"); const hasConfirmation = await verifyScreenContains("Order Confirmed"); const orderNumber = await readFieldValue(new Region(400, 200, 150, 30)); ``` ## Verification and Assertions ### Visual Verification ```typescript async function assertImageVisible( imageName: string, message: string = "Image not found" ): Promise { try { await screen.waitFor(imageResource(imageName), 5000); } catch { // Capture screenshot for debugging await screen.capture(`./failures/assertion-${Date.now()}.png`); throw new Error(message); } } async function assertImageNotVisible( imageName: string, message: string = "Image should not be visible" ): Promise { try { await screen.find(imageResource(imageName)); await screen.capture(`./failures/assertion-${Date.now()}.png`); throw new Error(message); } catch (error) { if (error.message === message) throw error; // Expected: image not found } } // Assertions await assertImageVisible("success-icon.png", "Success icon should appear after save"); await assertImageNotVisible("error-dialog.png", "No error should appear"); ``` ### Text Verification ```typescript async function assertTextInRegion( region: Region, expectedText: string ): Promise { const actualText = await screen.read(region); if (!actualText.includes(expectedText)) { await screen.capture(`./failures/text-assertion-${Date.now()}.png`); throw new Error(`Expected "${expectedText}" but found "${actualText}"`); } } // Verify status bar shows "Ready" const STATUS_BAR = new Region(0, 780, 400, 20); await assertTextInRegion(STATUS_BAR, "Ready"); ``` ## Complete Example: Legacy CRM Automation ```typescript import { screen, keyboard, mouse, Key, Button, Region, sleep, centerOf, straightTo, imageResource, getWindows, } from "@nut-tree/nut-js"; import { exec } from "child_process"; // Configuration screen.config.resourceDirectory = "./crm-images"; screen.config.confidence = 0.9; keyboard.config.autoDelayMs = 50; class LegacyCRMAutomation { private appTitle = "Acme CRM v3.2"; async launch(): Promise { exec('start "" "C:\\Program Files\\AcmeCRM\\crm.exe"'); for (let i = 0; i < 60; i++) { await sleep(1000); const windows = await getWindows(); for (const win of windows) { if ((await win.title).includes(this.appTitle)) { await win.focus(); await sleep(1000); return; } } } throw new Error("CRM failed to launch"); } async login(username: string, password: string): Promise { // Wait for login dialog await screen.waitFor(imageResource("login-dialog.png"), 10000); // Fill credentials const usernameField = await screen.find(imageResource("username-field.png")); await mouse.move(straightTo(centerOf(usernameField))); await mouse.click(Button.LEFT); await keyboard.type(username); await keyboard.pressKey(Key.Tab); await keyboard.releaseKey(Key.Tab); await keyboard.type(password); // Click login await this.clickButton("login-button.png"); // Wait for main window await screen.waitFor(imageResource("main-dashboard.png"), 30000); } async searchCustomer(searchTerm: string): Promise { // Navigate to customers await this.navigateMenu(["menu-customers.png", "menu-search-customer.png"]); // Fill search await screen.waitFor(imageResource("search-dialog.png"), 5000); await keyboard.type(searchTerm); await this.clickButton("search-button.png"); // Wait for results await sleep(2000); } async createNewCustomer(customer: { firstName: string; lastName: string; email: string; phone: string; }): Promise { // Open new customer form await this.navigateMenu(["menu-customers.png", "menu-new-customer.png"]); await screen.waitFor(imageResource("customer-form.png"), 5000); // Fill form fields using Tab navigation const fields = [ customer.firstName, customer.lastName, customer.email, customer.phone, ]; const firstField = await screen.find(imageResource("first-name-field.png")); await mouse.move(straightTo(centerOf(firstField))); await mouse.click(Button.LEFT); for (const value of fields) { await keyboard.type(value); await keyboard.pressKey(Key.Tab); await keyboard.releaseKey(Key.Tab); await sleep(100); } // Save await this.clickButton("save-button.png"); // Wait for confirmation and extract customer ID await screen.waitFor(imageResource("customer-created.png"), 10000); // Read the customer ID from a known region const idRegion = new Region(300, 200, 100, 25); const customerId = await screen.read(idRegion); // Close dialog await this.clickButton("close-dialog.png"); return customerId.trim(); } private async clickButton(buttonImage: string): Promise { const button = await screen.find(imageResource(buttonImage)); await mouse.move(straightTo(centerOf(button))); await mouse.click(Button.LEFT); await sleep(300); } private async navigateMenu(menuPath: string[]): Promise { for (let i = 0; i < menuPath.length; i++) { const item = await screen.waitFor(imageResource(menuPath[i]), 5000); await mouse.move(straightTo(centerOf(item))); if (i === menuPath.length - 1) { await mouse.click(Button.LEFT); } else { await sleep(400); } } } async close(): Promise { await keyboard.pressKey(Key.LeftAlt, Key.F4); await keyboard.releaseKey(Key.LeftAlt, Key.F4); await sleep(500); // Handle "Save changes?" dialog if it appears try { await screen.waitFor(imageResource("save-changes-dialog.png"), 2000); await this.clickButton("dont-save-button.png"); } catch { // No dialog, app closed } } } // Test usage async function runTest() { const crm = new LegacyCRMAutomation(); try { await crm.launch(); await crm.login("testuser", "testpass123"); const customerId = await crm.createNewCustomer({ firstName: "Jane", lastName: "Smith", email: "jane.smith@example.com", phone: "555-987-6543", }); console.log(`Created customer with ID: ${customerId}`); await crm.searchCustomer("jane.smith@example.com"); // Verify customer appears in search results await screen.waitFor(imageResource("customer-row.png"), 5000); } finally { await crm.close(); } } ``` ## Best Practices ### Image Reference Management ``` reference-images/ ├── dialogs/ │ ├── login-dialog.png │ ├── save-dialog.png │ └── error-dialog.png ├── buttons/ │ ├── submit-button.png │ ├── cancel-button.png │ └── ok-button.png ├── fields/ │ ├── username-field.png │ └── password-field.png └── states/ ├── loading-spinner.png └── success-checkmark.png ``` ### Capture Images at Test Resolution Always capture reference images at the same resolution and scale you'll run tests at. ### Handle Timing Variability Legacy apps can be slow. Build in appropriate waits: ```typescript // Good: Wait for element to appear await screen.waitFor(imageResource("result.png"), 30000); // Avoid: Fixed sleep without verification await sleep(5000); await clickButton("result.png"); // May fail if app is slow ``` ### Isolate Test State Reset application state between tests: ```typescript afterEach(async () => { // Close any open dialogs await keyboard.pressKey(Key.Escape); await keyboard.releaseKey(Key.Escape); await sleep(300); // Return to home screen await keyboard.pressKey(Key.LeftControl, Key.Home); await keyboard.releaseKey(Key.LeftControl, Key.Home); }); ``` --- # Blog Posts ## Introducing the @nut-tree/element-inspector Plugin for Windows **Date**: 2024-12-17 **URL**: https://nutjs.dev/blog/remote-plugin **Author**: Simon Hofmann **Tags**: devlog, release **Description**: Explore the new `@nut-tree/element-inspector` plugin for Windows, designed to enhance the capabilities of nut.js by providing an advanced implementation of the `ElementInspectionProviderInterface`. It all started two years ago with nut.js v2.0.0 - and now I'm finally able to show you how this plan has turned out! ![Remote screen capture](/assets/remote_screen.gif) ## Design For Flexibility With release v2.0.0 nut.js received a big overhaul of its internal architecture. What used to be a rather tightly coupled framework has been changed into a tight user-facing API and a loose core framework. This architecture gives nut.js the ability to be extensible and allows users to tweak it to their specific needs. A perfect example for this are two existing plugins, [`@nut-tree/playwright-bridge`](/plugins/playwright-bridge) and [`@nut-tree/selenium-bridge`](/plugins/selenium-bridge). Instead of re-inventing the wheel by coming up with a custom web testing DSL for nut.js, we make use of the framework's extensibility to integrate nut.js with e.g. Playwright. Custom provider packages make it possible to attach a Playwright context or a Selenium driver to nut.js. This allows you to use nut.js features like image search or OCR in your web tests, re-using the well-known public APIs of nut.js. ## Thinking Outside Your Box All of this is possible because nut.js essentially relies on four core building blocks: - It uses a `screen` to capture screen content. - It uses a `mouse` to control the mouse cursor. - It uses a `keyboard` to simulate keyboard input. - It uses a `clipboard` to copy and paste text. These four core building blocks can be implemented in a number of different ways. They might be provided by your local machine, but they can just as well be "virtualized" by e.g. a headless Playwright session. But if you take another look at the four core building blocks, maybe some other sources for these building blocks might come to your mind? Let's look at the following code sample which was used in the recording at the top of this page: ```js import { useNlMatcher } from "@nut-tree/nl-matcher" useNlMatcher() import { Key, imageResource, keyboard, screen, sleep } from "@nut-tree/nut-js" await sleep(3000); await keyboard.type(Key.LeftWin); await screen.waitFor(imageResource("start.png"), 10000, 1000); await keyboard.type("notepad"); await keyboard.type(Key.Enter); await screen.waitFor(imageResource("notepad.png"), 10000, 1000); await keyboard.type("Hello world! nut.js ❤️ you!"); ``` Looks like a pretty standard workflow to open Notepad and type "Hello world! nut.js ❤️ you!" on a Windows machine, right? But I'm on a Mac, sitting in the first floor of my house? And the only Windows machines I own are located in my office in the basement? Let me show you how I solved this problem without taking the stairs down to my office: ```js import { useNlMatcher } from "@nut-tree/nl-matcher" useNlMatcher() import { Key, imageResource, keyboard, screen, sleep } from "@nut-tree/nut-js" import { connect } from "@nut-tree/remote-plugin"; const { useRemoteScreen, useRemoteKeyboard, isConnected, disconnect } = await connect({ hostname: process.env.REMOTE_HOSTNAME, port: parseInt(process.env.REMOTE_PORT), username: process.env.REMOTE_USERNAME, password: process.env.REMOTE_PASSWORD, }); console.log(`Connected: ${isConnected()}`); useRemoteScreen(); useRemoteKeyboard(); await sleep(3000); await keyboard.type(Key.LeftWin); await screen.waitFor(imageResource("start.png"), 10000, 1000); await keyboard.type("notepad"); await keyboard.type(Key.Enter); await screen.waitFor(imageResource("notepad.png"), 10000, 1000); await keyboard.type("Hello world! nut.js ❤️ you!"); disconnect(); ``` Similar to how the Playwright bridge works, the remote plugin is able to provide an abstract `screen` and `keyboard`, `mouse` and `clipboard` by connecting to a remote system, which allows you to easily scale out your tests to multiple machines. You simply `connect` to a remote machine and set up your providers via `useRemoteScreen()` and `useRemoteKeyboard()` and your tests are no longer running on your local machine. All hidden behind the stable public APIs of nut.js you are now running on-screen image search or OCR on a remote machine. The best thing here is the fact that this is once again based on existing technologies. Remote plugins are built on top of VNC or RDP, so there's no setup on the remote machine required. > Remote plugins can turn nut.js into a scriptable VNC or RDP client, which means if you're able to log in to a remote machine, you're able to use nut.js to perform remote testing or automation! ## Streamlining Remote Testing with Native Framework Support Let's talk about the benefits of native remote testing and automation. I have a background in test automation, so I'm no stranger to this topic. One common approach to perform remote automation was to perform it through a remote desktop application, e.g. a VNC client. But instead of solving a problem, one actually gains another one. By automating an application through another application you get twice the work for twice the amount of ~~fun~~ instability. Additionally, you can't simply run a remote desktop application in the background, because if it's not visible on your screen, your automation script is not able to process it's content. So you either have your screen blocked by the remote application or you have to be creative to work around this. One approach would be to run the automation script in a Docker container that provides a headless VNC session. But these Docker images tend to become rather big and cumbersome to maintain. Having native remote support in the framework solves both of these issues. You don't need to run an additional application, you can simply connect to a remote machine and start automating it's content, directly accessing it's screen content. This eliminates a whole bunch of hassles and makes remote testing a more stable and reliable experience. You also don't need a Docker image providing a full desktop environment, a lightweight `node` image is more than enough. This in turn reduces the amount of resources that need to be allocated and makes remote testing more performant. Especially in scenarios where you want to monitor desktop applications on multiple machines, this is a huge win! ## Conclusion Native remote support is a huge milestone for nut.js! It'll bring a whole new level of stability and flexibility to your remote testing setup. The plugin is not yet released, but if it already caught your attention, feel free to reach out to [info@dry.software](mailto:info@dry.software) to learn more about it! --- ## Release v4.5.0 **Date**: 2024-11-05 **URL**: https://nutjs.dev/blog/release450 **Author**: Simon Hofmann **Tags**: release **Description**: nut.js v4.5.0: Unicode Support, Input Monitoring, and More! # Announcing nut.js v4.5.0: Unicode Support, Input Monitoring, and More! We are thrilled to announce the release of **nut.js v4.5.0**! This update brings a host of new features and improvements that make automation scripting with nut.js more powerful and flexible than ever before. From enhanced Unicode support to a revamped low-level codebase, innovative input monitoring capabilities, and updates to our macOS screen capture functionality, there's a lot to unpack. Let's dive in! ## Unicode Support Now in Core One of the most significant enhancements in this release is the integration of **general Unicode support directly into the nut.js core**. Previously, handling Unicode characters was exclusive to the `@nut-tree/bolt` plugin. With this update, you can seamlessly work with Unicode characters without relying on external plugins. ### What Does This Mean for You? - **Simplified Workflow**: No need to install additional plugins for Unicode support. - **Broader Language Support**: Easily automate tasks involving non-ASCII characters. - **Improved Compatibility**: Enhanced support for internationalization and localization efforts. ## Refactored Low-Level Code: A Step Towards Cross-Language Compatibility We've undertaken a comprehensive **refactoring of the low-level code**, separating it from the Node.js bindings and encapsulating it into a dedicated **CMake library**. This architectural overhaul has several exciting implications: ### Benefits of the Refactoring - **Easier Development**: A cleaner codebase simplifies maintenance and feature addition. - **Cross-Language Potential**: Opens up possibilities to create bindings for other programming languages. - **Performance Improvements**: Optimized code leads to faster execution times. ## Updated macOS Screen Capture Functionality In response to deprecations in macOS APIs, we've **rewritten the screen capture code for macOS**. Some of the previously used APIs have been deprecated, and the new API we've adopted is only available starting from * *macOS 14**. To ensure compatibility across different macOS versions, we've introduced **separate packages**: - **For macOS versions earlier than 14**: We maintain a package that uses the older APIs, ensuring continued support for users on earlier macOS versions. - **For macOS 14 and later**: A new package leverages the latest APIs, providing improved performance and stability. ### What This Means for You - **Automatic Compatibility**: The appropriate screencapture package is automatically selected based on your macOS version during installation. - **Seamless Transition**: Users on older macOS versions can continue using nut.js without any disruption. - **Enhanced Performance**: Users on macOS 14 and later benefit from the improvements of the new APIs. ### How to Get the Update Simply update nut.js to the latest version, and the correct screen capture package will be handled for you. No additional steps are required on your part. ## Introducing Input Monitoring with `@nut-tree/bolt` The `@nut-tree/bolt` plugin has been supercharged with a new * *[InputMonitor interface](https://nut-tree.github.io/apidoc/interfaces/_nut_tree_provider_interfaces.InputMonitor.html) **, allowing you to monitor mouse and keyboard input events like never before. ### EventEmitter Integration Both the **mouse and keyboard instances in nut.js are now EventEmitters**. This means you can: - **Register Callbacks**: Attach functions to respond to specific keyboard commands or mouse positions. - **Reactive Programming**: Build scripts that react in real-time to user inputs. ### Input Monitoring vs. Global Hotkeys It's important to distinguish **Input Monitoring** from **Global Hotkeys**: - **Non-Intrusive**: Input Monitoring observes events without overriding existing functionalities. - **Flexible Control**: Unlike global hotkeys, which can interfere with system shortcuts, Input Monitoring allows for passive observation and reaction. ### Use Case: Implementing a "Kill Switch" One practical application of this feature is setting up a **keyboard shortcut as a "kill switch"** to abort an automation script. This is incredibly handy for both development and production environments where you might need to halt a script immediately. #### How to Set Up a Kill Switch ```javascript import {keyboard, Key, mouse} from '@nut-tree/nut-js'; keyboard.on('keyDown', async (evt) => { if (isKeyEvent(evt, Key.Escape) && withModifiers(evt, [Key.LeftControl])) { // User pressed Ctrl + Esc, exit the script immediately process.exit(-1); } }); mouse.on('mouseMove', async (evt) => { // Track mouse movements here console.log(`You're at position ${evt.targetPoint.x}, ${evt.targetPoint.y}`); if (isAtPosition(evt, new Point(100, 100))) { await mouse.click(Button.LEFT); } }); system.startMonitoringInputEvents(); ``` ### Additional Example: One-Time Event Listener with `once` Another benefit of using EventEmitter is the ability to set up **one-time event listeners** using the `once` method. This allows you to execute a callback function only the first time an event occurs. #### Use Case: Performing an Action on First Mouse Click Suppose you want to perform a specific action the first time the user clicks the mouse. You can achieve this easily with `once`: ```javascript const {mouse} = require('@nut-tree/nut-js'); mouse.once('mouseDown', async (evt) => { console.log(`Mouse button ${evt.button} clicked for the first time.`); // Perform your one-time action here }); ``` This approach is especially useful when you need to initialize something or trigger an event only once, without manually removing the event listener afterward. ### Tutorial: Reacting on User Input Check out our tutorial on [nut.js Input Monitoring](/tutorials/input-monitoring) to learn how to build dynamic applications. ### Acknowledgment This feature was developed in collaboration with [GAL Digital GmbH](https://www.gal-digital.de/de). We thank their team for the excellent QA support and partnership. ## Committing to Open Source Sustainability We believe in giving back to the community that supports us. To date, we have **donated $1,766.18** in open source sponsorships to projects and maintainers that our work relies on. ![nut.js Open Source Sponsorships](/assets/sponsoring.png) ### Why This Matters - **Community Growth**: Supporting open source projects fosters innovation and collaboration. - **Sustainability**: Financial contributions help maintainers continue their important work. - **Shared Success**: Our project thrives because of the open source ecosystem, and we aim to keep it thriving. ## What's Next? One big feature done, many more to come! There's already something big on the horizon, so stay tuned for the next release. ## Join the Conversation Stay updated with the latest news and updates on the [nut.js Discord server](https://discord.gg/BM9ncaQRJ5). Thank you for your continued support! Happy automating! Simon --- ## I'm giving up — on open source **Date**: 2024-05-02 **URL**: https://nutjs.dev/blog/i-give-up **Author**: Simon Hofmann **Tags**: announcement **Description**: Sustainable open source will stay a dream A little over a year ago I wrote a blog post about [open source and why I'm charging money for some of my plugins](/blog/money). Sadly, one year later I've reached a point where I'm just not willing to continue the way I did before. So this is my letter of resignation. ## Why? Ever since I first started to use Linux I was fascinated by the idea of open source. Almost everything I built on my own is open source, and I'm still contributing to upstream projects I'm using if I encounter things I can improve. I sponsored an open source project for the first time over ten years ago when I was still in university, because I always held the belief that if a project is valuable for me, it's worth supporting it. And if I don't have the time to contribute to it myself, I should at least support the people that do. Of course, there are people that explicitly do not want any kind of sponsorship, but if they do, I'm happy to help. Working on an open source project is still work, and if you're doing a good job, you should be rewarded for it. I also always believed that if you ever started a project that is valuable for companies, they would support you in return, at least that's the reason why my own company is a monthly sponsor of [Verdaccio](https://verdaccio.org/) and why I sponsored maintainers of libraries I'm relying on. So, due to these naive believes, I started to work on nut.js under Apache-2.0 license, because I thought that if companies and individuals alike are able to permissively use my software, they would also be willing to support me in return. Now, before you start judging me that I'm only in for the money, wouldn't you agree that it sounds awesome to work on an open source project full time, and still be able to pay your bills? Did it work out? No. All I got was complaints. In the beginning, people were complaining that the image search plugin was baked into the core of nut.js, and that they were forced to use certain compatible versions of node or Electron. Then they started complaining that the image search plugin was not compatible with Apple Silicon. Which I made clear I would not be able to fix without a machine to test on. So if nobody is willing to lend me a machine or sponsor me, so I could get one myself, it's not going to happen. You think anyone made a move? Nope. Once I decided to take the investment myself, but charge for the new plugin, I suddenly turned into the greedy asshole that's not giving away everything for free. Same goes for companies. Nobody cares about you as long as everything is working smoothly, but as soon as they encounter a problem, guess who comes knocking on my door? ~~This public issue on the nut.js repo~~, where I'm publicly accused of something that's entirely not true was the final nail in the coffin. -- EDIT -- The previously linked issue caused quite a lot of reactions, some good, some not so good. Even GitHub reached out to me, asking if it would be okay if they would remove the issue, as it took a turn into some difficult direction. For reference, here's the condensed version of it, with all names and accounts removed: ``` > Not only did you sell out, you also removed all the old versions that were released under an open source license so that others couldn't continue to use out-of-support versions. `@nut-tree/plugin-ocr` has never been publicly available, nor was any other of the paid addons that were released after introducing non-free addons **almost 2.5 years ago**. > You should do a better job updating your documentation so that people do not waste their time like I did. This change to closed source was announced where, exactly? All of your READMEs and documentation sites do not mention this This is mentioned on the nutjs.dev landing page and e.g. [pricing page](https://nutjs.dev/pricing/pricing). The core readme refers to the website for additional information, so I’d say it’s fair enough to expect people to actually read it. > tl;dr get off GitHub and npm entirely if you want to do the closed-source thing, kthx. It was a pleasure to meet you, have a great day! ``` -- END EDIT -- This has happened several times already. I've been insulted for the things I do with nut.js on Discord, Reddit and now GitHub, but this time, I'm not just sucking it up. It may seem to you that open source is great because it's free to use. Truth is, it certainly is not free. Someone is paying a price for it, and if it's not the user, it's the maintainer. Everyone's time is valuable, and you may want to spend it wisely. If it's fun to spend time on something, that's great. But if it becomes a burden, it's not fun anymore. And if people start insulting you for something you're doing in your free time, it's time to stop. Open source is great, but it's not sustainable. We self-sabotaged ourselves over decades, and now we're at a point where it's hard to turn back. Publishing source code for the greater good is a noble cause, but to be honest, I think that over the years, using "open source" has become an excuse to avoid paying for software. And who's to blame if something goes wrong? The maintainers, of course. I've played this game with nut.js for almost six years, but it's coming to an end now. ## What's next? All of my packages around nut.js will cease to exist publicly on npm. Ready-to-use packages will only be available through the private nut.js package registry, which requires an active subscription to be used. The GitHub repo will remain public, so if you want to continue using nut.js on your own, you'll have to take care of building, testing and hosting packages yourself. If you want to save yourself some time and work, you should grab a license today, because prices will also increase with the release of additional plugins. Existing subscribers will not be affected by this increase. ## Will I stop working on nut.js entirely? Definitely not. I'll continue to work on nut.js, but updates to the repo will happen with a delay. New features, patches, bug fixes and security updates will be made available to subscribers first. As I said, if you want to continue using nut.js, you'll have to take care of building, testing and hosting packages yourself. All the best Simon --- ## Automate outside the box - nut.js remote plugins **Date**: 2024-04-30 **URL**: https://nutjs.dev/blog/element-inspection **Author**: Simon Hofmann **Tags**: announcement **Description**: Introducing native remote support for nut.js - automate distant machines via VNC or RDP protocols. I'm thrilled to announce the release of the latest nut.js plugin, `@nut-tree/element-inspector`, now available in beta for Windows users. This powerful new tool is designed to enhance the capabilities of nut.js by providing an implementation of the `ElementInspectionProviderInterface`. This release marks a significant step forward in our commitment to developing robust tools for GUI automation and testing. ## Key Features and Availability `@nut-tree/element-inspector` enables developers to delve into the GUI elements of a window, providing a detailed JSON output of the element hierarchy. This not only aids in understanding the structure of the window but also facilitates precise interaction with its elements. ### Current Availability - **Beta Release:** The plugin is currently in beta. We encourage you to try it out and provide feedback to help us improve. - **Windows Exclusive:** At this time, the plugin is available exclusively for Windows platforms. We plan to expand support to other platforms in future updates. ## Getting Started To get started with `@nut-tree/element-inspector`, simply install it using npm: ```shell npm i @nut-tree/element-inspector ``` ## Pricing Plans `@nut-tree/element-inspector` is included in both our [Solo](/pricing/solo) and [Team](/pricing/team) plans, making it accessible to individual developers and teams alike. ## Deep Dive into Usage The plugin's functionality can be explored through various use cases: 1. **Retrieve and Inspect Window Elements:** Quickly fetch and inspect elements up to a specified depth within the window hierarchy. 2. **Search for Specific Elements:** Utilize the `WindowElementQuery` to find specific elements based on various properties like type, title, and role. 3. **Handle Multiple Elements:** Efficiently manage multiple instances of elements and perform collective operations with them. 4. **Dynamic Element Handling:** Employ methods to wait for elements to appear or stabilize within the window, which is crucial for dynamic GUIs. Here's a quick snippet to demonstrate the new possibilities to search and interact with window elements: ```js import { useConsoleLogger, ConsoleLogLevel, screen, windowWithTitle, mouse, Button, straightTo, centerOf } from "@nut-tree/nut-js"; import {useBolt} from "@nut-tree/bolt"; import { useElementInspector } from "@nut-tree/element-inspector"; useElementInspector(); import {elements} from "@nut-tree/element-inspector/win"; useConsoleLogger({logLevel: ConsoleLogLevel.DEBUG}); useBolt(); const vs = await screen.find(windowWithTitle(/Visual Studio Code/)); await vs.focus(); const fileMenu = await vs.find(elements.menuItem({title: "File"})); if (fileMenu.region != null) { await screen.highlight(fileMenu.region); await mouse.move(straightTo(centerOf(fileMenu.region))); await mouse.click(Button.LEFT); } ``` ## Documentation Head over to the [element-inspector documentation](/plugins/element-inspector) for detailed information on the plugin's capabilities. ## Troubleshooting and Tips Given the complex nature of element inspection, here are a few tips for troubleshooting common issues: - **Enable Accessibility Modes:** Some applications require specific accessibility settings to be enabled for full functionality. - **Stability of Dynamic Elements:** When interacting with dynamic elements like unfolding menus, adding slight delays can ensure accurate element captures. ## Future Plans Looking ahead, we are excited to expand the capabilities of `@nut-tree/element-inspector` with more features and broader platform support. Your feedback during this beta phase is invaluable as we strive to refine and enhance the plugin. Stay tuned for updates, and happy inspecting! All the best Simon --- ## We have company! **Date**: 2024-04-11 **URL**: https://nutjs.dev/blog/we-have-company **Author**: Simon Hofmann **Tags**: announcement **Description**: What's coming to nut.js? It's been quite a while since I wrote a blog post, and since I didn't write one for the fifth anniversary of nut.js, I thought I'd write one with some big news now. So, what's the deal? ## nut.js is now backed by a company Yes, you read that right, nut.js is in fact backed by a company. But before you become too excited, there are no VC millions involved. I registered my own company here in Germany and it's called [dry Software UG (haftungsbeschränkt)](https://dry.software). (Don't blame for the website, that's a placeholder while I'm working on it.) I did this for a couple of reasons: 1. The thought of running my own software company stuck with me for quite a while now 2. I wanted to have a legal entity to back nut.js, or more generally, all of my various software projects, present and future 3. Over the past years, I've already been running a layman's version of a software business, exchanging private package access for sponsorship money 4. I noticed that it's easier for companies to pay for software than for sponsoring random individuals on GitHub (that's sad for open source, but it seems to be the harsh truth for a lot of companies) Will this now cause any immediate changes to nut.js? No, not really. I'll keep working on nut.js as I did before. **But** if you or your company are interested in nut.js and want professional support before buying into a third-party tool, dry Software got you covered. ## nut.js private package access As mentioned above, I've been running a layman's version of a software business for quite a while now. I've been exchanging private package access for sponsorship money. This worked, but it also had several drawbacks: - I had to manually add people to the private package access list on npmjs.com - I also (who would've thought?) had to manually remove people from the private package access list on npmjs.com - Every user came with fixed costs, so I had no possibility to e.g. hand out trial access. - In summary, it was a lot of manual work and quite cumbersome. So I set out to find a better solution. I've been quietly working on this for a while now and it's finally time to announce it: **You can now buy a subscription to get immediate access to private packages!** **Private packages are served via a private npm registry, pkg.nutjs.dev!** **You can choose if you want to get access to just a single package (e.g. @nut-tree/nl-matcher), or all at once!** **You can choose if you want to pay monthly or yearly! (You'll get one month for free on yearly payment!)** **You can get proper invoices for yourself or your company!** ### How does it work? Subscriptions are sold via [LemonSqueezy](https://www.lemonsqueezy.com) and all your subscription data resides there. nutjs.dev and the private registry will use this data to determine access rights to packages. Once you purchased a subscription you'll receive an email invite to create a user account on nutjs.dev. (The user account is required to manage access tokens for the private registry.) Once you created your account, you'll be able log in via Magic Link to access your profile. ![Access your profile from the account menu](/assets/account_menu.png) On your profile page, you'll be able to see your subscriptions and custom licenses. ![Your profile page](/assets/profile.png) For each subscription you'll get a short summary of the most important information. ![Subscription info](/assets/subscription_info.png) Last but not least, you'll be able to manage your access tokens for the private registry. ![License activation](/assets/license_activation.png) Depending on your plan, you'll be able to activate one or more token. Simply enter an id for the token and click the `Activate` button. Of course you can also revoke them again as well, to e.g. support off-boarding of employees or project members. What's left now is to configure npm to use the private `pkg.nutjs.dev` registry. Therefor you'll need to add [scoped auth](https://docs.npmjs.com/cli/v10/configuring-npm/npmrc#auth-related-configuration) settings for the `@nut-tree` organisation to your `.npmrc` file. Clicking the `Copy .npmrc settings` button will copy the required settings to your clipboard, so you can easily paste it into your `.npmrc` file. You can verify your setup by running `npm whoami --registry=https://pkg.nutjs.dev` in your terminal, which should return the id you entered when creating a new token. Please also take a look at the [FAQ](https://nutjs.dev/#faq) for more information and let me know if you encounter any problems. ## What's next? One more thing I'm now easily able to do is to **offer discounts**. If you want to purchase a subscription, use the code `WEHAVECOMPANY` on checkout to save 20% each month. ![Discount code in checkout form](/assets/discount_code.png) This discount applies until Nov. 20th, so you should make sure to grab it while it’s available! As I said, I'll keep working on nut.js as I did before. I have a couple of ideas for new features and improvements, e.g. improving the OCR plugin, multi-monitor support, remote execution, input monitoring, an improved image matcher... the list is long and it keeps on growing. But I also want to focus on improving documentation. Docs are important, but they tend to be neglected when the next shiny feature is calling. I want to do better! All the best Simon --- ## A short summary of the nut.js plugin architecture! **Date**: 2024-02-28 **URL**: https://nutjs.dev/blog/short-plugin-summary **Author**: Simon Hofmann **Tags**: devlog **Description**: A short summary of the nut.js plugin architecture! Let's talk about the nut.js plugin architecture! ## How plugins are used in nut.js First of all: nut.js is kind of an "umbrella package" It orchestrates several other packages, so called "providers", in a way that every single provider does one thing and all the pieces are falling into place. One provider might be responsible for retrieving screen info like width/height and its current content, while a different provider is responsible to handle keyboard input. Yet another provider might handle on-screen text search or mouse input. ## Why are provider plugins a good thing for users? Every provider is implementing a defined interface. You can take a look at all currently defined interfaces [here](https://nut-tree.github.io/apidoc/modules/_nut_tree_provider_interfaces.html) This is a cool thing for several reasons. 1. You can mix and match different providers to fit your needs 2. You can easily build your own providers to fix implementation details without having to wait for a nut.js package release 3. You can wrap existing provider packages ### Example (Artificial) example: You want to avoid at all costs that some script accidentally presses a certain key combination, so you... - Wrap an existing provider package, e.g. @nut-tree/libnut - You implement [the KeyboardProviderInterface](https://nut-tree.github.io/apidoc/interfaces/_nut_tree_provider_interfaces.KeyboardProviderInterface.html) - Add your own check which keys are to be pressed - You throw an error if it's the "forbidden key combination" This way you only have to add an additional import in your script + a call to register the custom provider and you're done. No additional changes to your existing code is needed. ### Changing providers Another huge benefit is that you can easily switch e.g. the underlying OCR engine to perform on-screen text search. nut.js only instruments provider packages via their interfaces, so it does not matter how the provider is implemented, as long as it adheres to the interface. I hope this sheds some light onto why provider plugins make nut.js really flexible and open for customisation! --- ## Why screen capture is broken on macOS Ventura **Date**: 2023-04-16 **URL**: https://nutjs.dev/blog/apple-silicon-screencapture-memory **Author**: Simon Hofmann **Tags**: devlog **Description**: Searching for the root cause of broken screen capture on macOS Ventura # Apple fooled me once, but they won’t fool me again Since a few years now I do all my work on a Mac, which means I’m also using my Mac for nut.js development. Usually this works flawlessly and I’m really happy with macOS and its ecosystem. But in December 2020 I’ve been fooled by Apple for the first time. ## The old problem A former colleague of mine created an [issue on the nut.js repo](https://github.com/nut-tree/nut.js/issues/194), stating that capturing screen regions on macOS yielded broken images. He also mentioned that capturing the whole screen worked fine, which left me puzzled. Capturing the full screen content worked fine, but capturing a region of the screen resulted in tilted images. ![Broken image](https://user-images.githubusercontent.com/25754312/101634107-83d7e880-3a28-11eb-98af-c4031a9c89bd.png) I spent some time investigating the issue and suspected a memory layout problem, but one day, after upgrading to macOS Big Sur, the issue was gone. Since there were not too many macOS users in the nut.js userbase back then, I just shrugged the issue off and moved on. ## The new problem Several years passed since the initial appearance of beforementioned issue and I didn’t receive any further reports about it. The amount of macOS users using nut.js continued to grow and I was happy with the stability of the macOS implementation. That was until two month ago when suddenly several users reported on-screen image search to not work and screen capture throwing errors. I tried to reproduce the issue on both my 2018 Intel MacBook Pro and my M1 Mac Mini, but failed to do so. I was tempted to slap a `Can’t reproduce, closing` onto the issue, but since I knew something like that had happened before, I decided to look for the actual root cause this time. ## How nut.js captures screen content On macOS nut.js (or more specifically, one of it’s underlying provider plugins, e.g. [libnut](https://github.com/nut-tree/libnut-core) uses the [CoreGraphics framework](https://developer.apple.com/documentation/coregraphics) to capture screen content. It aquires a reference to the image data of the main screen using the following code: ```c CGDirectDisplayID displayID = CGMainDisplayID(); CGImageRef image = CGDisplayCreateImageForRect(displayID, CGRectMake( rect.origin.x, rect.origin.y, rect.size.width, rect.size.height ) ); ``` `rect` is either a user-defined custom region, or defaults to the full screen size. Following that it determines the required buffer size using `CFDataGetLength`, allocates memory and creates a copy of the image data for further use via `CFDataGetBytes`. This piece of code was working fine for years, but suddenly it seemed to have stopped working for some users, so let’s dissect this code a bit more. ### macOS screen image data How much data are we actually talking about here? Let’s use the default screen resolution of my 2021 16“ MacBook Pro, which is `1728x1117` pixels, as reported by the OS. So we’re talking about a total of `1728 * 1117 = 1,930,176` pixels. We can determine the amount of bytes for a single pixel by using `CGImageGetBitsPerPixel`, which tells us that a single pixel is represented by 32 bits, or 4 bytes. `1,930,176 * 4 = 7,720,704` bytes. But there’s one more thing missing: **pixel density**! Depending on the display type (e.g. Retina, non-Retina), the amount of pixels per inch (ppi) differs. This is called the [backing scale factor](https://developer.apple.com/documentation/appkit/nswindow/1419459-backingscalefactor?language=objc) of a display. `‌size_t expectedBufferSize = rect.size.width * pixelDensity * rect.size.height * pixelDensity * bytesPerPixel;` Accounting for the backing scale factor in x and y direction, which is 2 for my MacBook Pro with Retina display, we get a total of `7,720,704 * 2 * 2 = 30,882,816` bytes. Now that we’ve got the numbers, let’s compare them to what we actually receive. ## Size matters I compared the amount of bytes we receive from `CGDisplayCreateImageForRect` to the amount of bytes we expect to receive on: 1. The built-in display of my 2021 16“ Apple Silicon MacBook Pro 2. The built-in display of my 2018 15“ Intel MacBook Pro 3. An external 2K display connected to my 2021 16“ Apple Silicon MacBook Pro This turned out to be quite interesting. ### External 2K display When connected to my external display I couldn’t find any issues with the screen capture. Both expected and reported buffer sizes matched perfectly and I was able to capture the screen content without any problems. ### Built-in display (2018 15“ Intel MacBook Pro) On my 2018 15“ Intel MacBook Pro with a reported resolution of `1680x1050` pixels everything worked as expected as well: ``` Expected buffer size: 28,224,000 Reported buffer size: 28,224,000 ``` ### Built-in display (2021 16“ Apple Silicon MacBook Pro) When using the built-in display of my 2021 16“ Apple Silicon MacBook Pro, I was able to reproduce the issue. ``` Expected buffer size: 30,882,816 Reported buffer size: 30,883,840 Diff: 1,024 ``` As you can see, the reported buffer size is 1,024 bytes larger than the expected buffer size. Cutting off these additional 1,024 bytes from the reported buffer size, we get the expected buffer size of `30882816` bytes and full-screen screen capture worked again. But that did not really solve the problem, just a single case. What if you apply scaling? Or want to capture only a sub-region of the screen? I ran the test again, but with two types of scaling applied: - Higher resolution: 2056x1329 ``` Reported buffer size: 43,892,736 Expected buffer size: 43,718,784 Diff: 173,952 ``` Cutting off the excess bytes did not fix the image buffer. Instead, it resulted in tilted images similar to the one I provided at the beginning of this post. Looking at the tilted image I suspected a **byte width** problem. The expected byte width would be `size_t expectedByteWidth = expectedBufferSize / (rect.size.height * pixelDensity);` and we can determine the byte width of the `CGImageRef` using ‌ `CGImageGetBytesPerRow`: ``` Reported byte width: 16,512 Expected byte width: 16,448 Diff: 64 ``` Every row of the image has an additional 64 bytes, which explains why the image is tilted and shows a black diagonal. - Lower resolution: 1496x967 ``` Reported buffer size: 23,281,664 Expected buffer size: 23,146,112 Diff: 135,552 ``` The same problem happened with a lower screen resolution: ``` Reported byte width: 12,032 Expected byte width: 11,968 Diff: 64 ``` - Smaller screen region: 100x100 ``` Reported buffer size: 180,224 Expected buffer size: 160,000 Diff: 164,224 ``` And with smaller screen regions: ``` Reported byte width: 896 Expected byte width: 800 Diff: 96 ``` These numbers show that image data is strided: ``` |XXXXX——|XXXXX——|XXXXX——|XXXXX——| ``` Manually assembling the image buffer solves this problem: ```cpp auto parts = bufferSize / reportedByteWidth; for (size_t idx = 0; idx < parts - 1; ++idx) { std::memcpy(buffer + (idx * expectedByteWidth), dataPointer + (idx * reportedByteWidth), expectedByteWidth ); } ``` Having this logic in place in cases where `expectedBufferSize < bufferSize ‌` fixes screen capture on macOS and should work across different screen resolutions and/or display types. After all these tests I came to the conclusion that this issue was re-introduced in macOS Ventura. Similar to the my first encounter with it it was not present in Monterey and appeared in Ventura. I’m curious whether it’ll disappear again in one of the next releases of macOS. Let’s see if Apple comes up with yet another way to break things in nut.js. All the best Simon --- ## Apple Silicon + Screen Capture = 💥 **Date**: 2023-03-22 **URL**: https://nutjs.dev/blog/apple-silicon-screencapture **Author**: Simon Hofmann **Tags**: devlog **Description**: Investigating broken screen capture on macOS Ventura on Apple Silicon Recently several Mac users reported crashes whenever they tried to capture their screen using nut.js. At first I suspected a one-off error, and I also wasn't able to reproduce the issue. But as more and more users started reporting the same issue, I started to collect some more information. So, I tried to reproduce the issue on both my Intel MacBook Pro and my Apple Silicon Mac Mini, but on both systems everything worked as expected. Since all reports were coming from Apple Silicon Macs, I neglected my Intel machine and focused on the Apple Silicon one. However, I was still unable to reproduce the issue. And to make matters worse, people started reporting that the issue did not occur on macOS Monterey, but only on macOS Ventura. An issue that only occurs on a particular piece of hardware and only on a particular operating system version... Great... To put this into perspective, two month had already passed until I had enough data to draw this conclusion. But how to deal with that? Well, some of you might have already noticed that I'm pretty serious about my work on nut.js and I didn't want to leave this rather big issue lingering around until it might disappear on its own. Call me crazy, but I ultimately replaced my M1 Mac Mini with an M1 MacBook Pro and started digging. With this new machine I was able to gather detailed data, showcasing that the issue was indeed only happening on Apple Silicon Macs running macOS Ventura. I created a small demo project that would reproduce the issue, subscribed to the Apple developer program and filed a bug report. Ok, so there's now a bug report to get this fixed, but what about the users who are affected by this issue until it get's (hopefully) resolved? Now that I knew where the problem originated from I tried to work around it. And fortunately, I was able to find a partial solution which is now available in the latest snapshot version of nut.js. So for now, I suggest to use the following workaround: 1. Install the latest snapshot version of nut.js: `npm i @nut-tree/nut-js@next` 2. Do not apply any scaling, but keep your screen resolution at the default This should mitigate the issue for now and I'll keep you updated on the progress of the bug report. ## Thanks again! I'd like to thank all project sponsors once again! As you can see, I'm really serious about my work on nut.js and I'm doing my best to keep it up to date and to address any issues that might arise. From time to time, this gets pricey, so your sponsorship is highly appreciated! Simon --- ## nut.js in a nutshell **Date**: 2023-02-06 **URL**: https://nutjs.dev/blog/in-a-nutshell **Author**: Simon Hofmann **Tags**: devlog **Description**: What's in the box? I recently drew up a mind map to get an overview of the whole nut.js ecosystem. Once I was done, I've been impressed by the amount of features provided out of the box. So without further ado, here's a quick overview of what's in the box: ![nut.js in a nutshell](/assets/nutshell.png) All the best Simon --- ## Why I'm Charging What I'm Charging **Date**: 2023-01-28 **URL**: https://nutjs.dev/blog/money **Author**: Simon Hofmann **Tags**: announcement **Description**: Sustainable open source is still a problem This is a post I've already rewritten a couple of times. Initially, I wanted to post it end of December last year, in response to a discussion on Discord. But the more I thought about it, the more I realised that there’s more to this topic than I initially anticipated. So, what is this all about? With release 2.0.0 of nut.js I made the decision to charge money for the `@nut-tree/nl-matcher` plugin. Why did I make this decision? Because the whole refactoring and the new plugin to achieve full Apple Silicon support took me several month to build + I had to make a financial sacrifice to get ahold of an Apple Silicon machine. I do love open source, but at that point I had to draw a line, regarding both time and money invested. So I decided to charge for the new plugin, (which is faster than the initial one, compatible with a wide range of node and Electron versions and also works with Apple Silicon chips), to cover some of my spendings. Since I made this decision, I've been continuously attacked that it would be too expensive, that I'd only be in for the money, or that I wouldn't carry the open source mind set by charging for my plug-ins. Funny enough, I've also received inquiries from (really big) companies for release dates of security fixes. Or startups, asking me to help them package their Electron application which uses nut.js. Alternatively they were asking for help with code related problems. What all of them have in common? Everyone of them was offended as soon as I mentioned that I'm not going to work for free for any company, just because they're using my framework. Also, none of them cared to sponsor my work, even though they were building products around it. Overall, this is fine, that's also what the license I chose permits, but if you want me to fix your problems, you better think about proper reimbursement. I do understand that 40$ per month is quite a lot of money for many people. On the other hand, I designed nut.js in a way that it's easy to build and use your own plug-in with it. There's even a full plug-in on GitHub to take a look at. If you don't want to spend money on a ready-to-use plug-in, you could build your own. If your time is too precious to invest it into building that add-on yourself, why do you think I should give away mine for free? All of the work I'm putting into nut.js happens some time between 10p.m. and way after midnight. At times it feels like I'm working two jobs. I'd love to open source all my plug-ins, but since they're the only way to get people to sponsor my work, it's also the only way to justify to my family why I sit in front of the computer late at night. If many people all sponsor a small amount, I'd not have to gate-keep my plugins with 40$ per person. That'd be great for all of us! Let's hope for the best and thanks for reading. Simon --- ## Release v3.0.0 **Date**: 2023-01-18 **URL**: https://nutjs.dev/blog/release300 **Author**: Simon Hofmann **Tags**: release **Description**: @nut-tree/nut-js@3.0.0 has been released! It's live! `@nut-tree/nut-js@3.0.0`! 🚀 A little over a year after the last major release I'm happy to announce the release of **nut.js v3.0.0** 🎉 ## What's new? Release v3.0.0 brings a lot of new features and improvements to nut.js, but here are two things I want to highlight: ### Logging Providers Often times nut.js is incorporated in an existing application and naturally, one would love to know what's going on under the hood. Logging is a diverse topic and there are multitudes of different logging frameworks out there. Frameworks that ship their own logging which is fully decoupled from your application's logging strategy are a bit cumbersome, so nut.js chose a different approach. Instead of shipping a logging framework, nut.js now ships with a logging provider interface, which allows you to plug in your own logging framework of choice. By default, nut.js ships a `ConsoleLogger`, but by implementing the `LogProviderInterface` you can register your own provider that wraps your already in place logging framework to include nut.js log output in your existing logging strategy. Here's an example of how to use the `ConsoleLogger`: ```js const { useConsoleLogger, ConsoleLogLevel, } = require("@nut-tree/nut-js"); useConsoleLogger({ logLevel: ConsoleLogLevel.DEBUG }); (async () => { // ... })(); ``` and what the log output looks like: ``` 2023-01-17T00:47:21.748Z - DEBUG - [nut.js] - Search region is valid 2023-01-17T00:47:23.652Z - DEBUG - [nut.js] - Language data for eng already exists in data path /private/tmp/d/node_modules/@nut-tree/ocr/language_data/tessdata, skipping 2023-01-17T00:47:26.305Z - INFO - [nut.js] - Using ocrConfidence override: 0.8 2023-01-17T00:47:26.306Z - DEBUG - [nut.js] - Found match! { location: Region { left: 328, top: 500, width: 141.5, height: 18 }, confidence: 90.44261169433594 } 2023-01-17T00:47:26.306Z - DEBUG - [nut.js] - 0 hooks triggered for match 2023-01-17T00:47:26.306Z - INFO - [nut.js] - Match is located at (328, 500, 141.5, 18) 2023-01-17T00:47:26.306Z - DEBUG - [nut.js] - Autohighlight is enabled 2023-01-17T00:47:26.307Z - INFO - [nut.js] - Highlighting (328, 500, 141.5, 18) for 0.5 with 25% opacity 2023-01-17T00:47:26.958Z - INFO - [nut.js] - Moving mouse to target point (398, 509) ``` The approach of connecting your own log provider is similar: ```ts import { useLogger, LogLevel, LogProviderInterface } from "@nut-tree/nut-js"; class Logger implements LogProviderInterface { debug(message: string, data: {} | undefined): void { // ... } error(error: Error, data: {} | undefined): void { // ... } info(message: string, data: {} | undefined): void { // ... } trace(message: string, data: {} | undefined): void { // ... } warn(message: string, data: {} | undefined): void { // ... } } useLogger(new Logger()); ``` ### screen.find `screen.find` got a lot of love in this release. It now supports additional types of `Finders`, so now you can not just find images on screen, but also text and/or windows. These new types of `Finders` seamlessly integrate into the existing `screen.find` API, so you can use them in the same way as you would use the `ImageFinder`: ```js const { mouse, screen, singleWord, sleep, useConsoleLogger, ConsoleLogLevel, straightTo, centerOf, Button, getActiveWindow, } = require("@nut-tree/nut-js"); const { preloadLanguages, Language, LanguageModelType, configure, } = require("@nut-tree/plugin-ocr"); configure({ languageModelType: LanguageModelType.BEST }); useConsoleLogger({ logLevel: ConsoleLogLevel.DEBUG }); screen.config.autoHighlight = true; screen.config.ocrConfidence = 0.8; function activeWindowRegion() { return getActiveWindow().then((activeWindow) => activeWindow.region); } (async () => { await preloadLanguages([Language.English], [LanguageModelType.BEST]); await sleep(5000); const result = await screen.find(singleWord("nut-tree")); await mouse.move(straightTo(centerOf(result))); await mouse.click(Button.LEFT); await screen.waitFor(singleWord("Native"), 15000, 1000, { providerData: { partialMatch: true }, }); const content = await screen.read({ searchRegion: activeWindowRegion() }); console.log(content); })(); ``` This way it's now possible to `waitFor` a window to appear, `findAll` occurrences of text on screen, or, just like it was already possible, `find` an image. ## What else? But wait, there's even more! 🤯 Not only did I release nut.js v3.0.0, but I also updated/built plugins! - I updated the available `ImageFinder` plugins to comply with the new `OptionalSearchParameters` interface. - I built a completely **NEW** plugin that gives you the ability to **read text** from screen! 👓 - In addition to the above, I built a completely **NEW** plugin that gives you the ability to **search text** on screen! 🔎 - I also built a new low-level provider plugin that comes with new features like unicode support for keyboard input and a provider to search for windows on screen. ⌨️ All of these packages are only available to sponsors of nut.js, so if you want to get your hands on them, you can do so by becoming a sponsor. See the [FAQ](https://nutjs.dev/#faq-sponsor-benefits) for additional information and the [plugins section](https://nutjs.dev/plugins) for available plugins. ## What's next? Get it on [npm](https://www.npmjs.com/package/@nut-tree/nut-js) [Check out the changelog](https://github.com/nut-tree/nut.js/releases/tag/v3.0.0) Please share your feedback on Twitter/GitHub/Discord! All the best Simon --- ## Release v2.3.0 **Date**: 2022-09-22 **URL**: https://nutjs.dev/blog/release230 **Author**: Simon Hofmann **Tags**: release **Description**: @nut-tree/nut-js@2.3.0 has been released! A few minutes ago I released `@nut-tree/nut-js@2.3.0`! 🚀 This release only ships one change, but that is a **major UX improvement!** Starting with release 2.3.0 macOS users will no longer have to fiddle with permissions themselves. nut.js now **automatically checks and asks for required permissions!** 🎉 Get it on [npm](https://www.npmjs.com/package/@nut-tree/nut-js) [Check out the changelog](https://github.com/nut-tree/nut.js/releases/tag/v2.3.0) Please share your feedback on Twitter/GitHub/Discord! All the best Simon --- ## Release v2.2.0 **Date**: 2022-09-15 **URL**: https://nutjs.dev/blog/release220 **Author**: Simon Hofmann **Tags**: release **Description**: @nut-tree/nut-js@2.2.0 has been released! A few minutes ago I released `@nut-tree/nut-js@2.2.0`! 🚀 This release comes with mostly bugfixes and some maintenance work, but there's **one** thing I want to highlight! Starting with release 2.2.0 it's no longer a requirement to have [the Visual C++ runtime redistributable](https://docs.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170) installed. For Windows users of nut.js this is quite a big deal, since it makes shipping solutions to other users and/or customers easier due to one runtime requirement less to take care off. 💪 In the following days I'll also work on updating available plugins to support this as well! Please share your feedback on Twitter/GitHub/Discord! All the best Simon --- ## Release v2.2.1 **Date**: 2022-09-15 **URL**: https://nutjs.dev/blog/release221 **Author**: Simon Hofmann **Tags**: release **Description**: @nut-tree/nut-js@2.2.1 has been released! A few minutes ago I released `@nut-tree/nut-js@2.2.1`! 🚀 First and foremost, this release updated [jimp](https://www.npmjs.com/package/jimp) to its latest release to fix critical security vulnerabilities. It also fixed a little bug on custom mouse movement where easing functions were not properly scaled. Please report any issues on the [nut.js repo](https://github.com/nut-tree/nut.js) All the best Simon --- ## jimp security advisory **Date**: 2022-08-27 **URL**: https://nutjs.dev/blog/jimp-security-advisory **Author**: Simon Hofmann **Tags**: security **Description**: Mitigating current security vulnerabilities in jimp ## Description A user recently opened an [issue](https://github.com/nut-tree/nut.js/issues/422) regarding open [security vulnerabilities through `jimp`](https://github.com/advisories/GHSA-xvf7-4v9q-58w6). The vulnerable package in question is [jpeg-js](https://www.npmjs.com/package/jpeg-js), a dependency of [@jimp/jpeg](https://www.npmjs.com/package/@jimp/jpeg). The good news is that the vulnerable package itself has been patched in version `0.4.4`. Unfortunately, [@jimp/jpeg](https://www.npmjs.com/package/@jimp/jpeg) has not yet been updated, which leads to the following two problems: The latest version of [@jimp/jpeg](https://www.npmjs.com/package/@jimp/jpeg) has pinned [jpeg-js](https://www.npmjs.com/package/jpeg-js) to version `0.4.2`, which is vulnerable. Previous versions are specifying a too wide version range (`^0.4.0`), which would also allow vulnerable versions of [jpeg-js](https://www.npmjs.com/package/jpeg-js). ## Advisory While we are waiting for a new upstream release of [@jimp/jpeg](https://www.npmjs.com/package/@jimp/jpeg) (there's already an [open PR](https://github.com/oliver-moran/jimp/pull/1090)) to do a patch release of nut.js, users can mitigate this issue by configuring an [override](https://docs.npmjs.com/cli/v8/configuring-npm/package-json#overrides) for `jpeg-js` to force usage of the fixed version. ```json numbered marked=12,13,14,15,16 { "name": "override-jpeg-js", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "overrides": { "@nut-tree/nut-js": { "jpeg-js": "0.4.4" } }, "dependencies": { "@nut-tree/nut-js": "^2.2.0" } } ``` All the best and sorry for the inconveniences Simon --- ## Mistakes have been made **Date**: 2022-08-10 **URL**: https://nutjs.dev/blog/mistakes-have-been-made **Author**: Simon Hofmann **Tags**: announcement **Description**: Sometimes we all have to admit that we made a mistake. So do I. Sometimes we all have to admit that we made a mistake. So do I. A while ago I thought it would be super smart to configure an automation using GitHub actions to mark and auto-close stale issues. In hindsight, I have to admit that this was a bad decision and I want to apologise. Auto-closing issues might be a valid approach to tame the amount of open tickets if your organisation is flooded with issues where people are e.g. to lazy to go through documentation. But this is not how nut.js issues work. In most cases, every single issue opened is a valid bug report or feature request and I intend to work on these things when I find the time to do so. Marking and auto-closing these issues didn't bring me any benefit. Contrary, it annoyed the people who opened these issues and are watching them for progress. To sum things up: I disabled the automation and re-opened all auto-closed issued. I want to apologise once more in case you felt offended by this mechanism! All the best Simon --- ## New Years News **Date**: 2022-01-04 **URL**: https://nutjs.dev/blog/new-years-news **Author**: Simon Hofmann **Tags**: release **Description**: nut.js had a great start into the new year with its long awaited 2.0.0 release! # Happy new year, everyone! nut.js had a great start into the new year with its long awaited 2.0.0 release! ## Ready for the future? First and foremost, this release is a huge invest in the future of nut.js by introducing a plugin system that allows users to provide their own implementation details for high-level functionality. Desktop automation is a really complex topic, and I came to the conclusion that establishing such a system is the only way to keep nut.js future-proof. While nut.js is equipped with lots of useful functionality you might be missing some feature for your particular job at hand. The most recent changes enable you to tweak implementation details to your specific needs, giving you full control. Additionally, having a plugin system allowed me to separate image matching code into its own module. This separation, grounded on user feedback, solves any compatibility issues between the nut.js core package and current / future releases of node and / or Electron. The limiting factor with regard to newer versions of node / Electron has always been `opencv4nodejs-prebuilt`, the native addon used to perform image matching. With this module moved out of the core package, the default nut.js installation no longer suffers from this limitation! If you do not require on-screen image search, you're all set to use any current node version! The cherry on top of all this is the fact that without the image matching module, the core package is usable on Apple Silicon. You can use nut.js on your precious M1 chips, no Rosetta required! ## Where's Waldo? All this previous lamenting about compatibility with recent node and Electron version, Apple M1 compatibility and so on does not answer one particular question: > What about image matching using recent node versions or an M1 machine? And I'm proud to say that there's an answer to this question: [`@nut-tree/nl-matcher`](https://nutjs.dev/docs/plugins/imagefinder#nut-treenl-matcher). Like I mentioned [in an earlier blog post](https://nutjs.dev/blog/does-it-spark-joy), it always bothered me that every new node or Electron release requires new builds for `opencv4nodejs-prebuilt`. Eventually I reached the point where I decided to tackle this issue once and for all and started re-implementing the image matching logic from scratch. I set up a new build pipeline to easily pre-compile and use the most recent OpenCV version and wrote a new node addon to perform image matching using the [Node-API](https://nodejs.org/api/n-api.html) and async workers. This way I could solve the version compatibility problem. But I even went one step further. Having this new and shiny addon and a streamlined build process I actually rented an M1 machine to tackle the Apple Silicon problem as well. It required some digging and going through CMake docs, but after several debugging runs I'm now able to build a single, universal binary for macOS that is compatible with both Intel and Apple Silicon chips! ## Time for a change! At this point you might ask yourself why I'm doing all the stuff you read about earlier. The simple answer is that this is my understanding of how qualitative software development should look like. nut.js has a thorough set of tests, for both the core package and plugins, automated builds for both releases and snapshots, auto-generated API docs and a nice API. It powers several open source projects but also gives a lot of freelancers an advantage over their competitors and even startups and established companies are using nut.js in their products. But this also comes with a cost. Having all these people rely on software I'm building puts pressure on me to keep things going. Also, maintaining this project for three platforms is hard at times, and with release 2.0.0 I ultimately crossed a line when I not just dedicated a huge amount of my time, but also my own money to the project to bring on-screen image search to M1 machines. That's why I updated two of my [GitHub sponsoring tiers](https://github.com/sponsors/s1hofmann/) to include the following: > Access to non-public nut.js packages I decided to keep the above-mentioned [`@nut-tree/nl-matcher`](https://nutjs.dev/docs/plugins/imagefinder#nut-treenl-matcher) package exclusive to project sponsors as a sponsoring perk. A few months ago I did a survey among nut.js users where I asked them what they are using nut.js for and whether they would be willing to support the project either financially or via community contributions to keep it going. An astonishing amount of people told me that they were using nut.js in commercial projects, be it in freelance / consulting projects or product development, but on the other hand, they are not willing to support the project in any form. That's a **BIG** issue and really had an impact on me that ultimately contributed to this decision. I know that I'm really fortunate to already have some sponsors, and I'm really thankful for their support, but overall it's also a thing about valuing someone else's work. Now before anyone gets mad about the sponsoring tiers I chose: - I'm confident that nut.js is worth it - I'm taking maintaining nut.js serious and hard work should be valued - It easily gives you an advantage over possible competitors From time to time I will also gift access to private packages to people that are actively participating in our community, because this should be valued as well! Existing sponsors will of course also get access! Alright, quite a lot to digest. Feel free to let it sink and if you feel like it, get in touch with me on Discord. Here's to a great 2022! All the best Simon --- ## Going on - devlog **Date**: 2021-10-28 **URL**: https://nutjs.dev/blog/going-on-devlog **Author**: Simon Hofmann **Tags**: devlog **Description**: Those of you who are keeping an eye on the nut.js repository might have noticed something — quite a big change is on the horizon! Those of you who are keeping an eye on the nut.js repository might have noticed something — quite a big change is on the horizon! As I already mentioned [in my previous post](https://nutjs.dev/blog/incremental-steps-devlog), I'm trying to reduce the amount of work required to keep nut.js compatible with upcoming node and Electron version. This became even more pressing since the Electron team switched to an eight weeks release cycle, so nut.js would require a new release every 8 weeks as well to keep up. The single reason for this is the `opencv4nodejs-prebuilt` dependency required to provide all the image matching functionality. Even though I'm making progress on building a replacement that does not suffer this version dependence, it'll take some more time to get it released. So, while working on solving this issue, I also decided to tackle two things mentioned in the nut.js user survey I posted a while ago. Turns out, nut.js users are undecided when it comes to image processing. Half of you are big fans, the others are unhappy about the drawbacks like node version dependence or package size. That, plus the wish for a plug-in system in nut.js, lead to the following strategy towards the next major release: 1. Establish a simple plug-in system to make underlying provider packages configurable 2. Pull existing image processing code and dependencies out of nut.js into a dedicated package that utilises the newly created plug-in system, making image matching an explicit opt-in feature 3. Keep working on a proper replacement package for image matching that will seamlessly replace the existing plug-in package ## What does this mean for nut.js users? Unfortunately, there will be breaking changes. At least for those of you using image matching functionality. The good thing is, it should be relatively simple to fix these. Instead of only installing nut.js via e.g. `npm i @nut-tree/nut-js`, you'll have to install an additional image matching plug-in package. ### Let's look at an example: 1. Create a new project and install nut.js ```bash npm init -y npm i @nut-tree/nut-js@next ``` 2. Create an `index.js` file with the following content: ```js const { screen, imageResource } = require("@nut-tree/nut-js"); (async () => { try { await screen.find(imageResource("img.png")); } catch (e) { console.error(e); } })(); ``` 3. Run the example and check its output ```bash node index.js Searching for img.png failed. Reason: 'Error: No ImageFinder registered' ``` The error output tells you that nut.js cannot search for your template image since no suitable `ImageFinder` has been registered. A similar error appears when trying to save a screenshot to disk: ```js const { screen, imageResource } = require("@nut-tree/nut-js"); (async () => { try { await screen.capture(imageResource("foo")); } catch (e) { console.error(e); } })(); ``` ```bash Error: No ImageWriter registered ``` This again tells us that a plug-in suitable for writing image files to disk is missing. #### How to we solve this? As a first step, let's install the newly created plug-in containing the image processing code extracted from nut.js. ```bash npm i @nut-tree/template-matcher ``` Once installed, all we have to do is import it in our code: ```js const { screen, imageResource } = require("@nut-tree/nut-js"); require("@nut-tree/template-matcher"); // THIS IS NEW (async () => { try { await screen.find(imageResource("img.png")); } catch (e) { console.error(e); } })(); ``` That's all it takes to put the previous image matching code back in place: ```bash Searching for img.png failed. Reason: 'Error: No match with required confidence 0.99. Best match: 0.9249920099973679 at (384, 26, 409.5, 62)' ``` ## I'm not using any image matching functionality, do I have to do anything? Short answer: No! If you haven't used any image matching functionality in your code, you won't encounter any problems. Actually, you'll even profit from it! By extracting the image matching code into a separate plug-in the nut.js base package itself: - Lost around 70 MB of required disk space - Is no longer dependent on a certain node version, so you're able to use it with e.g. node 17 right away ## Awesome, can I start using it? Currently, everything I just showed you is available in the latest `@next` release. **But please be warned!** This is still under active development and since it's a snapshot release, things might change/break at any time until it gets released as `2.0.0`! The main reason for this blog post is to inform users early on, so hopefully there won't be too many surprises once it's stable! With that in mind, feel free to test it and let me know what you think about it! Best regards, Simon --- ## Incremental steps - devlog **Date**: 2021-10-12 **URL**: https://nutjs.dev/blog/incremental-steps-devlog **Author**: Simon Hofmann **Tags**: devlog **Description**: My endeavour to get rid of nan-type bindings in nut.js, opencv4nodejs, to be precise, is making progress. First steps have been made! My endeavour to get rid of nan-type bindings in nut.js, `opencv4nodejs`, to be precise, is making progress. Slow, but steady. So far I redesigned how OpenCV itself is built and distributed for further use, solving one of the issues which are bothering me with the existing setup. This redesign eliminates a whole codebase I had to think through and follows a pretty lean approach for providing precompiled libs. The setup is already automated for all three platforms and I started a new project which uses these new artefacts. The process required some tweaking but right now everything fits together really smooth. As I already said, first steps have been made and I'm looking forward to take the next ones! Best regards Simon --- ## Does it spark joy? **Date**: 2021-06-27 **URL**: https://nutjs.dev/blog/does-it-spark-joy **Author**: Simon Hofmann **Tags**: goals **Description**: Sometimes you've got to ask this simple question. Does something spark joy for you? # Does it spark joy? Sometimes you've got to ask this simple question. Does something spark joy for you? In my case, something that definitely does not spark joy is updating `opencv4nodejs-prebuilt`. The reason being is the fact that `opencv4nodejs` relies on [ Native Abstractions for Node.js ](https://github.com/nodejs/nan "Native Abstractions for Node.js"), which in turn means that on every new node or Electron release, I'd have to provide additional prebuilds. In theory, this sound rather straightforward. Add a new node / Electron version to your CI config and off we go, but reality looks a bit different. Hands down, `opencv4nodejs` is great. It's configurable, compatible with recent OpenCV versions and async. However, it also has its peculiarities. It relies on a custom npm package which provides a wrapper around the whole OpenCV CMake build. This way it’s possible to configure and build OpenCV without leaving the well known npm ecosystem. While this approach definitely has its benefits, it’s an additional burden for me. When I started building nut.js I just used opencv4nodejs. But since I envisioned the installation of nut.js as straight forward as possible the whole „we build everything for you on install" just didn’t suite me. That’s how I ultimately ended up adopting two codebases by forking both the npm OpenCV build and opencv4nodejs itself to provide prebuilt binaries. As time went by, I noticed a few things I definitely wanted to change: - node ABI dependence: Every new node release requires new prebuilds which is the sole reason nut.js is running a bit late with support latest node versions - node-gyp: I don’t know why this build system is still so popular. I’m not a fan - Way too much functionality: opencv4nodejs aims to provide general purpose bindings for OpenCV while I’m only requiring a very small set of features - Mental load: I’m not particularly interested in deep diving another two codebases - Complexity: Even tough the setup works I’m the only one who knows why To put it in a nutshell (haha): I’m constantly looking for alternative solutions which solve the above points. I recently started digging into something which might be promising, I’ll let you know once I know! Best regards Simon --- ## Welcome **Date**: 2021-06-21 **URL**: https://nutjs.dev/blog/welcome **Author**: Simon Hofmann **Tags**: announcement **Description**: Huh? A dedicated nut.js page? Including a blog? Tell me more! Huh? A dedicated nut.js page? Including a blog? Tell me more! Well, it’s finally happening! After more than two years of active development nut.js receives the home it deserves. It’s all rough an sketchy so far, but I recently thought about the progress I made with nut.js and decided to step things up a little bit. I won’t go into much detail now, but rest assured that I have great plans for the future! Feel free to look around, there’s not a lot to see thus far. But also make sure to regularly check this page for updates! All the best Simon --- # Changelogs ## @nut-tree/bolt Changelog Changelog **URL**: https://nutjs.dev/changelog/bolt # 3.0.0 - Upgrade to Automationkit 1.0.7 - Bugfix: Fix memory leak when listing windows on Linux - Bugfix: Prevent segfault when interacting with certain windows on macOS - Feature: Enable native Linux ARM64 support - Enhancement: Ship both CommonJS and ESM - BREAKING: Require node v22 or later - BREAKING: Drop support for macOS < 14 # 2.3.0 - Upgrade to Automationkit 1.0.5 - Bugfix: Remove debug output - Feature: Enable native Windows ARM64 support ## 2.2.2 - Upgrade to Automationkit 1.0.4 - Bugfix: Fix positioning of highlight window on macOS - Enhancement: Improve stability of window focus operation on Windows and macOSA ## 2.2.1 - Update to bolt-core v2.2.1 - Bugfix: Upgrade to Automationkit 1.0.2 to avoid modifications to the Windows keyboard buffer to prevent issues with dead keys ## 2.2.0 - Update to bolt-core v2.2.0 - Feature: Introduce `isKeyPressed` function to check if a key is currently pressed - Feature: Implement mouse and keyboard input monitoring for on Windows and macOS - Enhancement: Enable usage of modifier keys on right side of keyboard - Maintenance: Update macOS screen capture implementation to deal with API deprecation in macOS 14 and later - Maintenance: Introduce separate package for macOS < 14 to maintain backwards compatibility - Maintenance: Switch to Automationkit submodule for shared low-level code ## 2.1.3 - Update to bolt-core v2.1.2 - Bugfix: Screen capture broken on macOS for certain capture region sizes ## 2.1.2 - Bugfix: Assign proper string values to Button enum which caused errors in underlying native implementations ## 2.1.1 - Maintenance: Improve release workflows ## 2.1.0 - Feature: Minimize/restore windows - Enhancement: Improve focus window ## 2.0.0 - Full rewrite ## 1.1.2 - Bugfix: Screen capture is broken on macOS 13 and later - Enhancement: Enable newly introduced keys to be used as modifiers - Bugfix: screen.highlight closes Electron window ## 1.1.1 - Bugfix: Fix rounding to mouse move on Window to fix mouse drift ## 1.1.0 - Bugfix: Update permission handling on macOS - Bugfix: Update permissionCheck.js to cache permission check results and only ask for permissions on actual function call by wrapping it in a HOF - Bugfix: Add rounding to mouse move on Window to fix mouse drift - Enhancement: Add additional keys ## 1.0.0 - Initial release --- ## @nut-tree/element-inspector Changelog Changelog **URL**: https://nutjs.dev/changelog/element-inspector ## 1.1.1 - Bugfix: Add missing `link` elements on macOS ## 1.1.0 - Feature: Hit testing — `elementAtPoint`, `windowAtPoint`, and `hitTest` to identify elements and windows at screen coordinates - Feature: Accessibility event monitoring — real-time a11y events across all platforms (`treeChanged`, `valueChanged`, `nameChanged`, `focusChanged`, `selectionChanged`, `menuOpen`, `menuClose`) with per-window subscriptions and event mask filtering - Bugfix: Fixed `findAll` with `in` relation, which returned empty results ## 1.0.1 - Enhancement: Improve package content ## 1.0.0 - Feature: Linux support via AT-SPI2 - Feature: `descendantOf` relation — find elements anywhere inside an ancestor, regardless of nesting depth - Feature: `ancestorOf` relation — find a container element that has a specific descendant - Feature: `after` relation — find elements that appear after a specific sibling - Feature: `before` relation — find elements that appear before a specific sibling - Feature: `siblingOf` relation — find elements that share a parent with a specific sibling - Feature: `nthChild` — select the nth child (0-based) among siblings - Feature: `firstChild` — select the first child of a parent - Feature: `lastChild` — select the last child of a parent - Feature: `notMatching` — exclude elements matching a given description, supports all property matchers including regex - Enhancement: `maxChildDepth` moved out of the description and into a dedicated search parameter - Enhancement: Moved to ESM with dual ESM/CJS module support - Bugfix: Fixed permission wrapper failure on macOS - BREAKING: Disable auto-registration and require explicit call to `useElementInspector` ## 0.4.3 - Improvement: Improve error output on failed regex queries. They now include the query pattern to make it easier to debug. ## 0.4.2 - Improvement: Bump peerDependency to `@nut-tree/nut-js` to `^4.7.0` to fix types for `WindowElementDescription` ## 0.4.1 - Bugfix: Fix platform dependent import mechanism ## 0.4.0 - Feature: macOS support - Feature: Native Windows ARM64 support ## 0.3.0 - Bugfix: Element search skipped root level element, making such queries impossible to resolve - Enhancement: Extended element info returned by `findElement` and `findElements` and `getElements` methods - Enhancement: Both `findElement` and `findElements` now support additional properties to filter elements by ## 0.2.1 - Bugfix: Properly collect elements in findElements ## 0.2.0 - Introduced element relations ## 0.1.5 - Initial public release --- ## @nut-tree/libnut Changelog Changelog **URL**: https://nutjs.dev/changelog/libnut # 3.0.2 - Bugfix: Emit proper syskeydown/up events for Alt key on Windows # 3.0.1 - Upgrade to Automationkit 1.0.7 - Bugfix: Fix memory leak when listing windows on Linux - Bugfix: Prevent segfault on macOS through dereference of invalid AXUIElementRef # 3.0.0 - Upgrade to Automationkit 1.0.7 - Bugfix: Fix memory leak when listing windows on Linux - Bugfix: Prevent segfault when interacting with certain windows on macOS - Feature: Enable native Linux ARM64 support - Enhancement: Ship both CommonJS and ESM - BREAKING: Require node v22 or later - BREAKING: Drop support for macOS < 14 # 2.9.0 - Bugfix: Remove debug output - Feature: Enable native Windows ARM64 support # 2.8.1 - Bugfix: Fix positioning of highlight window on macOS - Enhancement: Improve stability of window focus operation on Windows and macOS # 2.8.0 - Feature: Introduce `isKeyPressed` function to check if a key is currently pressed - Maintenance: Update macOS screen capture implementation to deal with API deprecation in macOS 14 and later - Maintenance: Introduce separate package for macOS < 14 to maintain backwards compatibility - Maintenance: Switch to Automationkit submodule for shared low-level code # 2.7.2 - Enhancement: Improve focus window on Windows to deal with focus-stealing prevention - Bugfix: Allow `meta` and `right_meta` as modifier keys # 2.7.1 - Bugfix: Re-build broken Windows release # 2.7.0 - Enhancement: Adding support for numpad 'Enter' key - Enhancement: Adding support for 'Fn' modifier key - Maintenance: Version upgrades, CI updates, etc. ## 2.6.0 - Feature: Move/focus/resize window - Enhancement: Adding support for numpad 'clear' key - Maintenance: Version upgrades, CI updates, etc. ## 2.5.2 - Bugfix: Screen capture broken on macOS 13 - Enhancement: Enable newly introduced keys to be used as modifiers - Bugfix: screen.highlight closes Electron window ## 2.5.1 - Bugfix: Fix rounding to mouse move on Window to fix mouse drift ## 2.5.0 - Bugfix: Update permissionCheck.js to cache permission check results and only ask for permissions on actual function call by wrapping it in a HOF - Bugfix: Add rounding to mouse move on Window to fix mouse drift - Enhancement: Add additional keys ## 2.4.1 - Bugfix: Update permission handling on macOS ## 2.4.0 - Bugfix: Fix `ReferenceError: b is not defined` - Enhancement: Improved permission handling on macOS - Bugfix: Limit calls to SetThreadDPIAwarenessContext to Windows 10 clients ## 2.3.0 - Bugfix: Segmentation Fault when retrieving window title - Enhancement: Automatically check and request required permissions on macOS ## 2.2.0 - Enhancement: Add Windows runtime files - Bugfix: Fix capture region x,y offset when DPI scaling on Windows - Bugfix: Fix wrong keycode for CapsLock ## 2.1.8 - Bugfix: Modifier keys are not properly released on macOS - Bugfix: Fix mouse clicks with modifiers on macOS ## 2.1.7 - Enhancement: Disable microsleep between keypresses on Windows and Linux - Enhancement: Add mappings for missing numpad keys - Enhancement: Added missing key mappings - Bugfix: Revert keyboardInput to use scancodes - Bugfix: Updated doubleClick implementation to fire two up/down cycles - Enhancement: Determine Windows doubleclick interval - Bugfix: Mouse click doesn't work on external monitor with negative x and y - Bugfix: macOS doubleclick fires two doubleclick events ## 2.1.6 - Enhancement: Numpad buttons don't work on Linux - Bugfix: Issue with keyboard.type in to Spotlight on MacOS ## 2.1.5 - Bugfix: Keypresses not properly caught on Windows - Enhancement: Enable some kind of warning / info message in case system requirements are not met ## 2.1.4 - Bugfix: Windows display scaling is applied in wrong direction ## 2.1.3 - Enhancement: Fix undefined behaviour of BufferFinalizer - Bugfix: Fix for screen highlight window minimized intead close - Enhancement: SendInput for mouse movement on Windows - Bugfix: Windows Scaling issues: screen functions broken - Enhancement: Support Apple Silicon - Bugfix: Remove scan code KEYUP block ## 2.1.2 - Maintenance: Upgrade CI - Enhancement: Fix compiler warnings - Enhancement: Windows: Support for HDPI displays - Enhancement: macOS: Support for M1 chips - Enhancement: Remove static keyboard delay ## 2.1.1 - Enhancement: Snapshot releases - Enhancement: Update CI configs - Enhancement: Split OS specific implementation into separate files - Enhancement: Linux: Update XGetMainDisplay to avoid receiving `Invalid MIT-MAGIC-COOKIE-1 key` - Enhancement: Enable GitHub Actions - Enhancement: Trigger snapshot build for - Bugfix: Region captures can't capture the whole screen ## 2.1.0 - Enhancement: Retrieve coordinates of current active window - Enhancement: Retrieve dimensions for a window specified via its window handle - Enhancement: Separate folders for OS specific implementation - Enhancement: Retrieve window name for a given window handle ## 2.0.1 - Bugfix: Fix hanging shift key after keyboard input on Windows --- ## @nut-tree/nl-matcher Changelog Changelog **URL**: https://nutjs.dev/changelog/nl-matcher ## 5.0.1 - Bugfix: Fix broken ESM output in 5.0.0 release ## 5.0.0 - Feature: Add linux-arm64 platform support - Enhancement: Ship both CommonJS and ESM - Maintenance: Update to work with latest nl-matcher core builds - BREAKING: Disable auto-registration and require explicit call to `useNlMatcher` ## 4.0.0 - Update nl-matcher core to v3.0.0 - Feature: Enable validation step to reduce false positives by default - Since this is a breaking change, you can still disable validation by setting `validateMatches` to `false` in `providerData` of the provider config ## 3.1.0 - Update nl-matcher core to v2.3.2 - Feature: Apply optional validation step to matcher results to reduce false positives ## 3.0.2 - Update nl-matcher core to v2.2.3 - Bugfix: Fix bounds check when processing unscaled images to avoid out of bounds errors ## 3.0.1 - Update matcher core to v2.2.2 - Bugfix: Drop alpha channel on input images to prevent issues with image processing ## 3.0.0 - Updated author information - Switched to new nut.js core packages - Dependency updates etc. ## 2.2.0 - Enhancement: Toggleable alpha masking ## 2.1.0 - Feature: Configurable scale steps - Feature: Grayscale image matching ## 2.0.1 - Bugfix: Properly parse optional providerData ## 2.0.0 - Feature: Bump OpenCV version to 4.7.0 - Feature: Add support for alpha masking - Enhancement: Refactor package structure ## 1.0.4 - Bugfix: Pin OpenCV version for stable releases ## 1.0.3 - Bugfix: Revert additional keypoint matching to stay consistent with template-matcher ## 1.0.2 - Enhancement: Adjust keypoint matching ratio - Enhancement: Performance improvements #2 - Enhancement: Performance improvements ## 1.0.1 - Version bump to fix versioning ## 1.0.0 - Initial stable release --- ## @nut-tree/nut-js Changelog Changelog **URL**: https://nutjs.dev/changelog/core ## 5.1.1 - Dependencies: Bump image-js to v1.5.0 ## 5.1.0 - Feature: Add accessibility event monitoring with support for `treeChanged`, `valueChanged`, `nameChanged`, `focusChanged`, `selectionChanged`, `menuOpen` and `menuClose` events. `Window` emits these as an `EventEmitter` and `waitFor` accepts `a11yEvent()`, `settled()` and `actionable()` descriptors - Feature: Add window event monitoring with support for `windowCreated`, `windowDestroyed`, `windowFocused`, `windowMoved`, `windowResized`, `windowMinimized`, `windowRestored`, `windowShown`, `windowHidden` and `windowTitleChanged` events. `ScreenClass` emits these as an `EventEmitter` and `waitFor` accepts `windowEvent()` and `settled()` descriptors - Dependencies: Bump libnut-core to v3.0.2 ## 5.0.1 - Bugfix: Fix broken clipboard provider after ESM migration ## 5.0.0 - Enhancement: Extend `screen.grab` and `screen.capture` to accept an optional region parameter, unifying the API with `grabRegion` and `captureRegion` - Enhancement: Extend `toShow` matcher to work with Windows - Enhancement: Extend WindowElement and WindowElementDescription with `isSelected` and `isChecked` properties - Enhancement: Extend `WindowElementDescription` with new relation queries (`descendantOf`, `ancestorOf`, `after`, `before`, `siblingOf`, `notMatching`) and position modifiers (`nthChild`, `firstChild`, `lastChild`) - Enhancement: Ship both CommonJS and ESM - Enhancement: Resolve package deprecations in dependencies and audit issues - Feature: Introduce Vitest matchers - Feature: Introduce `toMatchContentDescription` matcher which uses the `VisionFinderInterface` to match content expectations - Feature: Add vision LLM integration for natural language screen content matching via `VisionFinderInterface` and `contentMatchingDescription` query - Feature: Add support for screen recording either as a continuous stream or as snippets of defined length for e.g. error recordings - Feature: Add `toDataURL` to `Image` class for easier integration with LLM-based vision providers and other use cases - Feature: Add Linux ARM64 support for libnut provider - Feature: Make `keyboard.type`, `mouse.move`, `mouse.drag` and `sleep` abortable via an optional AbortSignal parameter - Bugfix: Fix broken Button mapping in libnut mouse provider - Maintenance: Migrate e2e tests to @playwright/test - Maintenance: Replace image-js / remove jimp dependency - Maintenance: Remove `node-abort-controller` dependency in favor of node built-in `AbortController` - BREAKING: Drop support for macOS < 14 - BREAKING: `screen.grabRegion` and `screen.captureRegion` were removed in favor of `screen.grab(region)` and `screen.capture(fileName, region)` - BREAKING: nut.js now requires at least Node.js v22 or later - BREAKING: `mouse.move` is now non-blocking - BREAKING: Type docs are no longer published to https://nut-tree.github.io/apidoc/. Instead, they are now integrated into the nutjs.dev website ## 4.7.0 - Enhancement: Add Windows ARM64 support for `@nut-tree/libnut` - Bugfix: Re-introduce `in` relation for `WindowElementDescription` to support `in` property in element queries ## 4.6.0 - Enhancement: Improve typedocs on [`pressKey`](https://nut-tree.github.io/apidoc/classes/_nut_tree_nut_js.KeyboardClass.html#pressKey) and [`releaseKey`](https://nut-tree.github.io/apidoc/classes/_nut_tree_nut_js.KeyboardClass.html#releaseKey) - Enhancement: Extend available properties on both [`WindowElement`](https://nut-tree.github.io/apidoc/interfaces/_nut_tree_nut_js.WindowElement.html) and [`WindowElementDescription`](https://nut-tree.github.io/apidoc/interfaces/_nut_tree_nut_js.WindowElementDescription.html), supporting a wider range of properties for search and retrieval ## 4.5.1 - Bugfix: Fix broken highlight function ([#609](https://github.com/nut-tree/nut.js/issues/609)) - Enhancement: Improve stability of window focus operation on Windows and macOS ## 4.5.0 - Feature: Introduce new `InputMonitor` interface - Dependencies: Update @nut-tree/libnut to v2.8.0 ## 4.4.1 - Bugfix: Properly export `toWaitFor` matcher ## 4.4.0 - Feature: New matchers to work with element queries - Enhancement: Update types of `toShow` matcher to accept Promises - Feature: New `toWaitFor` matcher which works with `waitFor` - Bugfix: Bump libnut-core to v2.7.3 to revert window focus change which broke input on Windows ## 4.3.0 - Enhancement: Modify text queries to support not only string, but also RegExp - Enhancement: Update return type of `window.getElements` to include missing `id` property - Enhancement: Update type definition of `WindowElementDescription` in nut.js core to include an `in` property - Enhancement: Update `mouse.setPosition` to support passing `Promise` - Feature: Allow users to reset all providers to their default - Feature: Allow users to reset providers for clipboard, keyboard, mouse, screen and window to their default - Enhancement: Improve `keyboard.type` input if `keyboard.config.autoDelayMs` is 0 - Bugfix: `Key.LeftSuper` and `Key.RightSuper` mapped to the wrong low-level key in libnut - Dependencies: Update @nut-tree/libnut to v2.7.2 ## 4.2.0 - Feature: Extend `Window` class with `minimize` and `restore` methods ## 4.1.0 - Enhancement: Extend `WindowProviderInterface` with `minimizeWindow` and `restoreWindow` functions - Feature: Introduce `WindowElementQuery` type and extend `Window` class with `find`, `findAll`, `waitFor` and find hooks ## 4.0.1 - Enhancement: Fix channel info for image loader to 4 to avoid processing errors ## 4.0.0 - Feature: Add ability to move/focus/resize windows - Enhancement: Add support for 'Clear' key on macOS - Feature: Optional default providers - BREAKING: OCR confidence override not working - Enhancement: New 'toHaveColor' Jest matcher - Enhancement: Improved error messages for timeouts - Maintenance: Migrate nut.js core and its default providers to a monorepo - Maintenance: Use PlayWright for E2E tests - Bugfix: `getActiveWindow` returns out-of-bounds coordinates for maximized windows - BREAKING: Require minimum node version of 16 ## 3.1.2 - Bugfix: Screen capture broken on macOS 13 - Enhancement: Enable newly introduced keys to be used as modifiers - Enhancement: Extend move API to handle single point case - Feature: Add color queries to search for pixels of a certain color - Bugfix: screen.highlight closes Electron window ## 3.1.1 - Bugfix: Fix mouse drift on Windows ## 3.1.0 - Enhancement: Typo fix - Enhancement: Additional keys ## 3.0.0 - Enhancement: Improve types of Jest matchers - BREAKING: Rename clipboard methods - Enhancement: Option to disable automatic request of permissions in macOS - BREAKING: Allow `screen.find` and other to work with non-image needles - BREAKING: Add bits per pixel and byteWidth info to image class - Bugfix: Installed Electron App crashes after upgrading to 2.3.0 - Maintenance: Add .nvmrc config - Enhancement: Define and export interfaces for keyboard/mouse/screen configs - Bugfix: 'RightShift' key is mapped to space - Maintenance: Introduce prettier - Bugfix: Win2012-R2: Error: The specified procedure could not be found - Feature: Logging provider ## 2.3.0 - Bugfix: Segmentation Fault when retrieving window title - Enhancement: Automatically check and request required permissions on macOS ## 2.2.1 - Enhancement: Scale easing function result by base speed before applying - Maintenance: Resolve security vulnerabilities ## 2.2.0 - Maintenance: Limit CI runs to PRs, not every push - Maintenance: Upgrade node version to 16 for all CI runs - Bugfix: Fix grave accent - Enhancement: Refine error messages on fetchFromUrl - Enhancement: Ship Windows runtime dependencies ## 2.1.1 - Bugfix: Modifier keys are not properly released on macOS - Bugfix: Fix mouse clicks with modifiers on macOS ## 2.1.0 - Bugfix: Keyboard methods `pressKey` and `releaseKey` ignore updated autoDelayMs - Enhancement: Add mappings for missing numpad keys - Enhancement: macOS double click - Maintenance: Both `mouse.leftClick` and `mouse.rightClick` should reuse `click` - Feature: New image loader to fetch remote images - Bugfix: Mouse methods `pressButton` and `releaseButton` should respect auto delay ## 2.0.1 - Bugfix: Issue with `keyboard.type` in to Spotlight on MacOS - Enhancement: Numpad buttons don't work on Linux ## 2.0.0 - Feature: Apple Silicon - Enhancement: Enable warning message for missing accessibility permissions on macOS - Enhancement: Add runtime typechecks for `screen.find` etc. - Bugfix: Fix Windows scaling issue - Maintenance: Refine types - Maintenance: Cleanup deprecated code - Enhancement: Support for mouse capturing games - Feature: Provide functions to convert images between BGR and RGB color mode - Feature: Audio keys support - Enhancement: Configurable interval for `waitFor` - Bugfix: Apply pixel density scaling on `colorAt` - Enhancement: Change `find` signature to only work on `Image` instances - Enhancement: Adjust `assert` class to new `Screen#find` parameter types - Feature: Get screen pixel color - Feature: Add `Screen#findAll` to enable matching multiple template occurrences - Enhancement: Make Screen#find accept `Promise` - Enhancement: Accepting a Buffer with image data for `Screen#find` - Enhancement: Get rid of adapter layer in favour of providerRegistry - Feature: Provide a default implementation for `ImageReader` and `ImageWriter` - Feature: Define interface for mouse movement type - Feature: Separate image matching code - Enhancement: Export `FileType` - Enhancement: Export `ImageWriterParameters` - Enhancement: Export provider interfaces - Feature: Introduce a registry for providers - Feature: Add methods to grab the current screen content as Buffer ## 1.7.0 - Enhancement: Trigger snapshot releases - Feature: Cancel screen.waitFor if needed - Enhancement: Move docs into separate repo - Feature: Support for node 16 and Electron 13 ## 1.6.0 - Feature: Create screenshot from region - Bugfix: Endless loop in timeout function for long-running actions returning undefined - Maintenance: Use default exports for all provider classes - Enhancement: imprecise error message if image is too large - Bugfix: `waitFor` does not properly cancel - Feature: Enable GitHub Actions - Enhancement: Use @nut-tree/libnut@next for snapshot releases - Enhancement: Requesting image search outside of screen boundaries fails with runtime error ## 1.5.0 - Enhancement: Window support - Bugfix: `screen.find` neglects offsets when providing a search region ## 1.4.2 - Maintenance: Refactor `locationparameters.class.ts` - Enhancement: Update npmignore - Maintenance: Refactor `image-processor.class.ts` - Enhancement: Update to `opencv4nodejs-prebuilt@5.3.0-2` - Enhancement: Add note about macOS permissions to readme - Enhancement: Stabilize drag & drop E2E test - Bugfix: Hanging shift key after keyboard input on Windows ## 1.4.1 - Bugfix: Electron + Windows problems ## 1.4.0 - Enhancement: API docs - Enhancement: Improve CI pipeline - Enhancement: Rename `MouseActionInterface` - Enhancement: Enhance test stability - Enhancement: Config cleanup - Enhancement: Improve error message when failing to locate images - Enhancement: Support for node 13 and 14 - Enhancement: Support for Electron - Enhancement: Native highlight ## 1.3.2 - Enhancement: Revisit mouse speed settings ## 1.3.1 - Bugfix: Wrong result size for scaled image search - Enhancement: Switch from robotjs to libnut - Enhancement: Update to OpenCV4 - Enhancement: Enable matrix builds ## 1.3.0 - Enhancement: Enabled prebuilt bindings for OpenCV ## 1.2.1 - Enhancement: Exported `Button` enum ## 1.2.0 - Bugfix: Drag & drop gestures were not working on macOS and Windows - Enhancement: `mouse` Public API now exposes `pressButton` and `releaseButton` methods ## 1.1.2 - Bugfix: Clipboard copy calls did not resolve ## 1.1.1 - Minor version upgrade for OpenCV dependency ## 1.1.0 - Feature: nut.js now comes with a precompiled version of OpenCV ## 1.0.1 - Bugfix: Check dimensions of ROIs to prevent access violations ## 1.0.0 - API overhaul - `find` hooks - `screen.waitFor` - Code cleanup ## 0.1.0-beta.3 - Improved error handling on image search ## 0.1.0-beta.2 - Changed default `screen.config.resourceDirectory` to use `process.cwd()` ## 0.1.0-beta.1 - Enabled pre-built OpenCV bindings via `opencv4nodejs-prebuilt` --- ## @nut-tree/playwright-bridge Changelog Changelog **URL**: https://nutjs.dev/changelog/playwright-bridge ## v2.0.0 - Maintenance: Replace image-js / remove jimp dependency - Enhancement: Ship both CommonJS and ESM - Feature: Introduce `toMatchContentDescription` matcher which uses the `VisionFinderInterface` to match content expectations ## v1.1.0 - Added support for `@playwright/test` matchers ## 1.0.0 - Initial release --- ## @nut-tree/plugin-ai-sdk Changelog Changelog **URL**: https://nutjs.dev/changelog/plugin-ai-sdk ## 1.0.0 - Initial stable release --- ## @nut-tree/plugin-azure Changelog Changelog **URL**: https://nutjs.dev/changelog/azure ## 2.1.1 - Bugfix: Endless loop in findPartialMatches [(#610)](https://github.com/nut-tree/nut.js/issues/610) ## 2.1.0 - Feature: Added support for RegExp text search queries ## 2.0.2 - Bugfix: Drop alpha channel on input images to prevent issues with image processing ## 2.0.1 - Bugfix: Fix case-sensitivity for comparisons when searching for text - Bugfix: Fix missing search for exact matches when processing textLine queries ## 2.0.0 - Enhancement: Implement narrow bounding boxes around lineQuery search results - Bump @nut-tree/nut-js peerDependency version to 4.0.0 ## 1.0.0 - Initial release --- ## @nut-tree/plugin-ocr Changelog Changelog **URL**: https://nutjs.dev/changelog/ocr ## 4.0.2 - Bugfix: Fix broken tagged release pipeline ## 4.0.1 - Bugfix: Fix broken ESM output in 4.0.0 release ## 4.0.0 - Feature: Support both x64 and ARM64 architectures across macOS, Linux and Windows - Enhancement: Improved image binarization for dark/light theme support - Enhancement: Ship both CommonJS and ESM - Maintenance: Removed `cross-fetch` dependency in favor of Node.js built-in `fetch` (requires Node >= 22) - Maintenance: Replaced Jest with Vitest for testing - Maintenance: Updated to latest nut.js core API - BREAKING: Disable auto-registration and require explicit call to `useOcrPlugin` - BREAKING: Replaced jimp with image-js for image processing ## 3.0.0 - Improvement: Don't binarize images by default as it might lead to lower quality results - Since this is a breaking change, you can still enable binarization by setting `binarize` to `true` in `preprocessConfig` of the provider config ## 2.1.1 - Bugfix: Endless loop in findPartialMatches [(#610)](https://github.com/nut-tree/nut.js/issues/610) ## 2.1.0 - Feature: Added support for RegExp text search queries ## 2.0.2 - Bugfix: Drop alpha channel on input images to prevent issues with image processing ## 2.0.1 - Bugfix: Fix case-sensitivity for comparisons when searching for text ## 2.0.0 - BREAKING: Updated processing of textLine queries to only return bounding box around match, not the whole line - Enhancement: Added auto wrapping for single language values in provider config ## 1.0.2 - Bugfix: Respect custom confidence values from match requests ## 1.0.1 - Enhancement: Return info about next best match in case of a miss - Enhancement: Remove language data file from plugin package - Enhancement: Default to configured LanguageModelType when preloading languages - Enhancement: Improve typing of readText ## 1.0.0 - First stable release ## 0.2.1 - Updated loading of language data from cache - Disabled sourcemaps - Removed image debug output in preprocess step ## 0.2.0 - Refined API ## 0.1.0 - Initial release --- ## @nut-tree/plugin-screenrecording Changelog Changelog **URL**: https://nutjs.dev/changelog/plugin-screenrecording ## 1.1.2 - Bugfix: Gracefully handle errors to properly shutdown recording worker ## 1.1.1 - Maintenance: Move typedoc to devDependencies ## 1.1.0 - Enhancement: Use a default of 30 seconds for buffered recording - Enhancement: Check for existing files when saving recordings to avoid accidental overwriting and data loss - Feature: Add option to enable overwriting existing files when saving recordings ## 1.0.0 - Initial stable release --- ## @nut-tree/plugin-screenrecording Changelog Changelog **URL**: https://nutjs.dev/changelog/provider-ai-sdk ## 1.0.1 - Improve confidence output ## 1.0.0 - Initial stable release --- ## @nut-tree/plugin-screenrecording Changelog Changelog **URL**: https://nutjs.dev/changelog/screenrecording ## 1.1.2 - Bugfix: Gracefully handle errors to properly shutdown recording worker ## 1.1.1 - Maintenance: Move typedoc to devDependencies ## 1.1.0 - Enhancement: Use a default of 30 seconds for buffered recording - Enhancement: Check for existing files when saving recordings to avoid accidental overwriting and data loss - Feature: Add option to enable overwriting existing files when saving recordings ## 1.0.0 - Initial stable release --- ## @nut-tree/selenium-bridge Changelog Changelog **URL**: https://nutjs.dev/changelog/selenium-bridge ## v2.0.0 - Maintenance: Replace image-js / remove jimp dependency - Enhancement: Ship both CommonJS and ESM ## 1.0.0 - Initial release --- # Legal Documents ## Cancellation Policy **URL**: https://nutjs.dev/legal/cancellation ## Right of withdrawal for downloads --- In the case of contracts for the delivery of data downloads (digital content not on a physical data carrier), the customer has the following right of revocation: ### Cancellation policy --- **Right of withdrawal** You have the right to cancel this contract within fourteen days without giving any reason. The revocation period is fourteen days from the day of the conclusion of the contract. To exercise your right of withdrawal, please inform us at ``` dry Software UG (haftungsbeschränkt) Lohbrunnstr. 17 93469 Rain, Germany ``` **E-mail:** sales@dry.software **Telephone:** +49 (0) 175 9923464 by means of a clear statement (e.g. a letter sent by mail or e-mail) about your decision to revoke this contract. To comply with the revocation period, it is sufficient to send the notification of the exercise of the right of revocation before the expiry of the revocation period. --- ## End User License Agreement (EULA) **URL**: https://nutjs.dev/legal/eula IMPORTANT: THIS END USER LICENSE AGREEMENT ("AGREEMENT" or "EULA") IS A LEGAL AGREEMENT BETWEEN YOU (EITHER AN INDIVIDUAL OR, IF LICENSED BY OR FOR AN ENTITY, THE ENTITY) AND THE PROVIDER OF NUTJS.DEV ("LICENSOR"). PLEASE READ THIS AGREEMENT CAREFULLY BEFORE INSTALLING OR USING THE SOFTWARE. BY INSTALLING, COPYING, OR OTHERWISE USING THE SOFTWARE, YOU AGREE TO BE BOUND BY THE TERMS OF THIS AGREEMENT. IF YOU DO NOT AGREE TO THE TERMS OF THIS AGREEMENT, DO NOT INSTALL, COPY, OR USE THE SOFTWARE. --- ## Definitions **"Licensor"** refers to dry Software UG (haftungsbeschränkt), the developer of nut.js. **"Software"** means the proprietary nut.js libraries along with all associated tools, plugins, modules, documentation, and any updates or modifications provided by Licensor. **"Private Registry"** means the private package registry operated by Licensor, accessible at https://pkg.nutjs.dev. **"License Seat"** means an individual authorization granted to a License User. License tiers are determined by the number of License Seats allocated. A Single-Seat License provides one License Seat for one License User, whereas a Multi-Seat License provides multiple License Seats, each entitling a distinct License User to access and use the Software. Sharing or transferring License Seats beyond the purchased quantity is strictly prohibited. **"License User"** means any person or entity (including technical accounts used in CI/CD environments) that accesses packages on the Private Registry by claiming a License Seat. **"Authorized User"** means an individual expressly authorized to use the Software under a valid license. Each Authorized User must be associated with a separate License Seat. **"Package License"** means a single-package, single-user license for products such as @nut-tree/bolt, @nut-tree/nl-matcher, or @nut-tree/plugin-ocr, which includes one Authorized User. **"Solo License"** means a license that authorizes use by one Authorized User. **"Team License"** means a license permitting use by multiple Authorized Users within a single organization. **"License Tier"** means the classification or category of the license purchased (e.g., Solo, Team, or Package). --- ## License Grant Subject to your compliance with this Agreement and payment of applicable fees, Licensor grants you a limited, non-transferable, non-exclusive, revocable license to: - Install and use the Software internally for development and deployment of your own applications; - Use the Software by the number of Authorized Users covered under your license tier. --- ## License Restrictions You may **NOT**: - Distribute, sublicense, rent, lease, sell, or otherwise make the Software available to any third party, except as agreed upon and permitted under a valid license; - Use the Software in a manner that enables unlicensed users to access its functionality; - Modify, reverse engineer, decompile, disassemble, or create derivative works of the Software; - Remove, obscure, or alter any proprietary notices or labels. - For Package or Solo licenses: Share the license with multiple people or entities. --- ## Use in Commercial Products Redistribution of the Software as part of a commercial product is not allowed unless explicitly permitted under a separate reseller license. --- ## Updates Licensor may, at its sole discretion, provide updates, patches, or new versions. These updates are subject to this Agreement. Licensor is under no obligation to maintain backward compatibility. Access to updates is limited to customers with an active license. --- ## Ownership Licensor retains all rights, title, and interest in and to the Software, including all intellectual property rights. This Agreement does not grant you any ownership rights. --- ## Term and Termination This Agreement is effective upon installation or use and continues until terminated. Licensor may terminate this Agreement immediately if you breach any terms. Upon termination: - You must cease all use of the Software; - Destroy all copies in your possession; - Remove the Software from all deployed applications if your license is no longer valid. --- ## Warranty Disclaimer THE SOFTWARE IS PROVIDED "AS IS" AND "AS AVAILABLE," WITHOUT WARRANTIES OF ANY KIND. LICENSOR DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. --- ## Limitation of Liability LICENSOR SHALL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. --- ## Indemnification You agree to indemnify and hold harmless Licensor, its affiliates, and licensors from any claim, liability, or demand arising out of your use of the Software or violation of this Agreement. --- ## Governing Law This Agreement shall be governed by and construed in accordance with the laws of the jurisdiction where Licensor is based, without regard to conflict of laws principles. --- ## Amendments Licensor reserves the right to modify this Agreement at any time. Material changes will be communicated in advance. Continued use of the Software after the effective date of the revised Agreement constitutes your acceptance. --- ## Contact Information For questions about this Agreement, please contact: [info@dry.software](mailto:info@dry.software) --- ## Legal Notice **URL**: https://nutjs.dev/legal/imprint ## Diensteanbieter --------------- dry Software UG (haftungsbeschränkt) Lohbrunnstr. 17 94369 Rain Deutschland ## Kontaktmöglichkeiten -------------------- E-Mail-Adresse: [info@dry.software](mailto:info@dry.software) Telefon: +49 (0) 175 9923464 ## Vertretungsberechtigte Personen ------------------------------- Vertretungsberechtigt: Simon Hofmann (Geschäftsführer) ## Register und Registernummer --------------------------- Handelsregister. Geführt bei: Amtsgericht Straubing Nummer: HRB 13494 ## Haftungs- und Schutzrechtshinweise ---------------------------------- Haftungsausschluss: Die Inhalte dieses Onlineangebotes wurden sorgfältig und nach unserem aktuellen Kenntnisstand erstellt, dienen jedoch nur der Information und entfalten keine rechtlich bindende Wirkung, sofern es sich nicht um gesetzlich verpflichtende Informationen (z. B. das Impressum, die Datenschutzerklärung, AGB oder verpflichtende Belehrungen von Verbrauchern) handelt. Wir behalten uns vor, die Inhalte vollständig oder teilweise zu ändern oder zu löschen, soweit vertragliche Verpflichtungen unberührt bleiben. Alle Angebote sind freibleibend und unverbindlich. Links auf fremde Webseiten: Die Inhalte fremder Webseiten, auf die wir direkt oder indirekt verweisen, liegen außerhalb unseres Verantwortungsbereiches und wir machen sie uns nicht zu Eigen. Für alle Inhalte und Nachteile, die aus der Nutzung der in den verlinkten Webseiten aufrufbaren Informationen entstehen, übernehmen wir keine Verantwortung. Urheberrechte und Markenrechte: Alle auf dieser Website dargestellten Inhalte, wie Texte, Fotografien, Grafiken, Marken und Warenzeichen sind durch die jeweiligen Schutzrechte (Urheberrechte, Markenrechte) geschützt. Die Verwendung, Vervielfältigung usw. unterliegen unseren Rechten oder den Rechten der jeweiligen Urheber bzw. Rechteinhaber. Hinweise auf Rechtsverstöße: Sollten Sie innerhalb unseres Internetauftritts Rechtsverstöße bemerken, bitten wir Sie uns auf diese hinzuweisen. Wir werden rechtswidrige Inhalte und Links nach Kenntnisnahme unverzüglich entfernen. --- ## Privacy Policy **URL**: https://nutjs.dev/legal/privacy ## Preamble -------- With the following privacy policy we would like to inform you which types of your personal data (hereinafter also abbreviated as "data") we process for which purposes and in which scope. The privacy statement applies to all processing of personal data carried out by us, both in the context of providing our services and in particular on our websites, in mobile applications and within external online presences, such as our social media profiles (hereinafter collectively referred to as "online services"). The terms used are not gender-specific. Last Update: 6. October 2023 ## Controller ---------- dry Software UG (haftungsbeschränkt) Lohbrunnstr. 17 94369 Rain Germany E-mail address: [info@dry.software](mailto:info@dry.software) ## Overview of processing operations --------------------------------- The following table summarises the types of data processed, the purposes for which they are processed and the concerned data subjects. ### Categories of Processed Data * Inventory data. * Payment Data. * Contact data. * Content data. * Contract data. * Usage data. * Meta, communication and process data. ### Categories of Data Subjects * Customers. * Employees. * Prospective customers. * Communication partner. * Users. * Business and contractual partners. ### Purposes of Processing * Provision of contractual services and fulfillment of contractual obligations. * Contact requests and communication. * Security measures. * Direct marketing. * Web Analytics. * Targeting. * Office and organisational procedures. * Conversion tracking. * Affiliate Tracking. * Managing and responding to inquiries. * Feedback. * Marketing. * Profiles with user-related information. * Provision of our online services and usability. * Information technology infrastructure. ## Relevant legal bases -------------------- **Relevant legal bases according to the GDPR:** In the following, you will find an overview of the legal basis of the GDPR on which we base the processing of personal data. Please note that in addition to the provisions of the GDPR, national data protection provisions of your or our country of residence or domicile may apply. If, in addition, more specific legal bases are applicable in individual cases, we will inform you of these in the data protection declaration. * **Consent (Article 6 (1) (a) GDPR)** - The data subject has given consent to the processing of his or her personal data for one or more specific purposes. * **Performance of a contract and prior requests (Article 6 (1) (b) GDPR)** - Performance of a contract to which the data subject is party or in order to take steps at the request of the data subject prior to entering into a contract. * **Compliance with a legal obligation (Article 6 (1) (c) GDPR)** - Processing is necessary for compliance with a legal obligation to which the controller is subject. * **Legitimate Interests (Article 6 (1) (f) GDPR)** - Processing is necessary for the purposes of the legitimate interests pursued by the controller or by a third party, except where such interests are overridden by the interests or fundamental rights and freedoms of the data subject which require protection of personal data. **National data protection regulations in Germany:** In addition to the data protection regulations of the GDPR, national regulations apply to data protection in Germany. This includes in particular the Law on Protection against Misuse of Personal Data in Data Processing (Federal Data Protection Act - BDSG). In particular, the BDSG contains special provisions on the right to access, the right to erase, the right to object, the processing of special categories of personal data, processing for other purposes and transmission as well as automated individual decision-making, including profiling. Furthermore, data protection laws of the individual federal states may apply. **Reference to the applicability of the GDPR and the Swiss DPA:** These privacy notices serve both to provide information in accordance with the Swiss Federal Act on Data Protection (Swiss DPA) and the General Data Protection Regulation (GDPR). ## Security Precautions -------------------- We take appropriate technical and organisational measures in accordance with the legal requirements, taking into account the state of the art, the costs of implementation and the nature, scope, context and purposes of processing as well as the risk of varying likelihood and severity for the rights and freedoms of natural persons, in order to ensure a level of security appropriate to the risk. The measures include, in particular, safeguarding the confidentiality, integrity and availability of data by controlling physical and electronic access to the data as well as access to, input, transmission, securing and separation of the data. In addition, we have established procedures to ensure that data subjects' rights are respected, that data is erased, and that we are prepared to respond to data threats rapidly. Furthermore, we take the protection of personal data into account as early as the development or selection of hardware, software and service providers, in accordance with the principle of privacy by design and privacy by default. TLS/SSL encryption (https): To protect the data of users transmitted via our online services, we use TLS/SSL encryption. Secure Sockets Layer (SSL) is the standard technology for securing internet connections by encrypting the data transmitted between a website or app and a browser (or between two servers). Transport Layer Security (TLS) is an updated and more secure version of SSL. Hyper Text Transfer Protocol Secure (HTTPS) is displayed in the URL when a website is secured by an SSL/TLS certificate. ## Transmission of Personal Data ----------------------------- In the context of our processing of personal data, it may happen that the data is transferred to other places, companies or persons or that it is disclosed to them. Recipients of this data may include, for example, service providers commissioned with IT tasks or providers of services and content that are embedded in a website. In such cases, the legal requirements will be respected and in particular corresponding contracts or agreements, which serve the protection of your data, will be concluded with the recipients of your data. ## International data transfers ---------------------------- If we process data in a third country (i.e. outside the European Union (EU), the European Economic Area (EEA)) or the processing takes place in the context of the use of third party services or disclosure or transfer of data to other persons, bodies or companies, this will only take place in accordance with the legal requirements. Subject to express consent or transfer required by contract or law, we process or have processed the data only in third countries with a recognised level of data protection, on the basis of special guarantees, such as a contractual obligation through so-called standard protection clauses of the EU Commission or if certifications or binding internal data protection regulations justify the processing (Article 44 to 49 GDPR, information page of the EU Commission: [https://ec.europa.eu/info/law/law-topic/data-protection/international-dimension-data-protection\_en](https://ec.europa.eu/info/law/law-topic/data-protection/international-dimension-data-protection_en)). Data Processing in Third Countries: If we process data in a third country (i.e., outside the European Union (EU) or the European Economic Area (EEA)), or if the processing is done within the context of using third-party services or the disclosure or transfer of data to other individuals, entities, or companies, this is only done in accordance with legal requirements. If the data protection level in the third country has been recognized by an adequacy decision (Article 45 GDPR), this serves as the basis for data transfer. Otherwise, data transfers only occur if the data protection level is otherwise ensured, especially through standard contractual clauses (Article 46 (2)(c) GDPR), explicit consent, or in cases of contractual or legally required transfers (Article 49 (1) GDPR). Furthermore, we provide you with the basis of third-country transfers from individual third-country providers, with adequacy decisions primarily serving as the foundation. "Information regarding third-country transfers and existing adequacy decisions can be obtained from the information provided by the EU Commission: [https://ec.europa.eu/info/law/law-topic/data-protection/international-dimension-data-protection\_en.](https://ec.europa.eu/info/law/law-topic/data-protection/international-dimension-data-protection_en) EU-US Trans-Atlantic Data Privacy Framework: Within the context of the so-called "Data Privacy Framework" (DPF), the EU Commission has also recognized the data protection level for certain companies from the USA as secure within the adequacy decision of 10th July 2023. The list of certified companies as well as additional information about the DPF can be found on the website of the US Department of Commerce at [https://www.dataprivacyframework.gov/.](https://www.dataprivacyframework.gov/) We will inform you which of our service providers are certified under the Data Privacy Framework as part of our data protection notices. ## Rights of Data Subjects ----------------------- Rights of the Data Subjects under the GDPR: As data subject, you are entitled to various rights under the GDPR, which arise in particular from Articles 15 to 21 of the GDPR: * **Right to Object: You have the right, on grounds arising from your particular situation, to object at any time to the processing of your personal data which is based on letter (e) or (f) of Article 6(1) GDPR, including profiling based on those provisions. Where personal data are processed for direct marketing purposes, you have the right to object at any time to the processing of the personal data concerning you for the purpose of such marketing, which includes profiling to the extent that it is related to such direct marketing.** * **Right of withdrawal for consents:** You have the right to revoke consents at any time. * **Right of access:** You have the right to request confirmation as to whether the data in question will be processed and to be informed of this data and to receive further information and a copy of the data in accordance with the provisions of the law. * **Right to rectification:** You have the right, in accordance with the law, to request the completion of the data concerning you or the rectification of the incorrect data concerning you. * **Right to Erasure and Right to Restriction of Processing:** In accordance with the statutory provisions, you have the right to demand that the relevant data be erased immediately or, alternatively, to demand that the processing of the data be restricted in accordance with the statutory provisions. * **Right to data portability:** You have the right to receive data concerning you which you have provided to us in a structured, common and machine-readable format in accordance with the legal requirements, or to request its transmission to another controller. * **Complaint to the supervisory authority:** In accordance with the law and without prejudice to any other administrative or judicial remedy, you also have the right to lodge a complaint with a data protection supervisory authority, in particular a supervisory authority in the Member State where you habitually reside, the supervisory authority of your place of work or the place of the alleged infringement, if you consider that the processing of personal data concerning you infringes the GDPR. ## Business services ----------------- We process data of our contractual and business partners, e.g. customers and interested parties (collectively referred to as "contractual partners") within the context of contractual and comparable legal relationships as well as associated actions and communication with the contractual partners or pre-contractually, e.g. to answer inquiries. We process this data in order to fulfill our contractual obligations. These include, in particular, the obligations to provide the agreed services, any update obligations and remedies in the event of warranty and other service disruptions. In addition, we process the data to protect our rights and for the purpose of administrative tasks associated with these obligations and company organization. Furthermore, we process the data on the basis of our legitimate interests in proper and economical business management as well as security measures to protect our contractual partners and our business operations from misuse, endangerment of their data, secrets, information and rights (e.g. for the involvement of telecommunications, transport and other auxiliary services as well as subcontractors, banks, tax and legal advisors, payment service providers or tax authorities). Within the framework of applicable law, we only disclose the data of contractual partners to third parties to the extent that this is necessary for the aforementioned purposes or to fulfill legal obligations. Contractual partners will be informed about further forms of processing, e.g. for marketing purposes, within the scope of this privacy policy. Which data are necessary for the aforementioned purposes, we inform the contracting partners before or in the context of the data collection, e.g. in online forms by special marking (e.g. colors), and/or symbols (e.g. asterisks or the like), or personally. We delete the data after expiry of statutory warranty and comparable obligations, i.e. in principle after expiry of 4 years, unless the data is stored in a customer account or must be kept for legal reasons of archiving. The statutory retention period for documents relevant under tax law as well as for commercial books, inventories, opening balance sheets, annual financial statements, the instructions required to understand these documents and other organizational documents and accounting records is ten years and for received commercial and business letters and reproductions of sent commercial and business letters six years. The period begins at the end of the calendar year in which the last entry was made in the book, the inventory, the opening balance sheet, the annual financial statements or the management report was prepared, the commercial or business letter was received or sent, or the accounting document was created, furthermore the record was made or the other documents were created. If we use third-party providers or platforms to provide our services, the terms and conditions and privacy policies of the respective third-party providers or platforms shall apply in the relationship between the users and the providers. * **Processed data types:** Inventory data (e.g. names, addresses); Payment Data (e.g. bank details, invoices, payment history); Contact data (e.g. e-mail, telephone numbers). Contract data (e.g. contract object, duration, customer category). * **Data subjects:** Prospective customers. Business and contractual partners. * **Purposes of Processing:** Provision of contractual services and fulfillment of contractual obligations; Contact requests and communication; Office and organisational procedures. Managing and responding to inquiries. * **Legal Basis:** Performance of a contract and prior requests (Article 6 (1) (b) GDPR); Compliance with a legal obligation (Article 6 (1) (c) GDPR). Legitimate Interests (Article 6 (1) (f) GDPR). **Further information on processing methods, procedures and services used:** * **Project and Development Services:** We process the data of our customers and clients (hereinafter uniformly referred to as "customers") in order to enable them to select, acquire or commission the selected services or works as well as associated activities and to pay for and make available such services or works or to perform such services or works. The required information is indicated as such within the framework of the conclusion of the agreement, order or equivalent contract and includes the information required for the provision of services and invoicing as well as contact information in order to be able to hold any consultations. Insofar as we gain access to the information of end customers, employees or other persons, we process it in accordance with the legal and contractual requirements; **Legal Basis:** Performance of a contract and prior requests (Article 6 (1) (b) GDPR). ## Use of online platforms for listing and sales purposes ------------------------------------------------------ We offer our services on online platforms operated by other service providers. In addition to our privacy policy, the privacy policies of the respective platforms apply. This is particularly true with regard to the payment process and the methods used on the platforms for performance measuring and behaviour-related marketing. * **Processed data types:** Inventory data (e.g. names, addresses); Payment Data (e.g. bank details, invoices, payment history); Contact data (e.g. e-mail, telephone numbers); Contract data (e.g. contract object, duration, customer category); Usage data (e.g. websites visited, interest in content, access times); Meta, communication and process data (e.g. IP addresses, time information, identification numbers, consent status). Content data (e.g. text input, photographs, videos). * **Data subjects:** Customers; Prospective customers. Business and contractual partners. * **Purposes of Processing:** Provision of contractual services and fulfillment of contractual obligations; Marketing; Conversion tracking (Measurement of the effectiveness of marketing activities). Provision of our online services and usability. * **Legal Basis:** Performance of a contract and prior requests (Article 6 (1) (b) GDPR). Legitimate Interests (Article 6 (1) (f) GDPR). **Further information on processing methods, procedures and services used:** * **Lemon Squeezy:** E-commerce platform, with features for invoicing, payment processing, economic analysis, customer and business partner management, as well as external communication and external interfaces; **Service provider**: Lemon Squeezy LLC, 222 South Main Street, Suite 500., Salt Lake City, UT 84101, USA; **Legal Basis:** Legitimate Interests (Article 6 (1) (f) GDPR); **Website:** [https://www.lemonsqueezy.com/](https://www.lemonsqueezy.com/); **Privacy Policy:** [https://www.lemonsqueezy.com/privacy](https://www.lemonsqueezy.com/privacy); **Data Processing Agreement:** [https://www.lemonsqueezy.com/dpa](https://www.lemonsqueezy.com/dpa). **Basis for third country transfer:** Standard Contractual Clauses ([https://www.lemonsqueezy.com/dpa](https://www.lemonsqueezy.com/dpa)). ## Providers and services used in the course of business ----------------------------------------------------- As part of our business activities, we use additional services, platforms, interfaces or plug-ins from third-party providers (in short, "services") in compliance with legal requirements. Their use is based on our interests in the proper, legal and economic management of our business operations and internal organization. * **Processed data types:** Inventory data (e.g. names, addresses); Payment Data (e.g. bank details, invoices, payment history); Contact data (e.g. e-mail, telephone numbers); Content data (e.g. text input, photographs, videos). Contract data (e.g. contract object, duration, customer category). * **Data subjects:** Customers; Prospective customers; Users (e.g. website visitors, users of online services). Business and contractual partners. * **Purposes of Processing:** Provision of contractual services and fulfillment of contractual obligations. Office and organisational procedures. * **Legal Basis:** Legitimate Interests (Article 6 (1) (f) GDPR). **Further information on processing methods, procedures and services used:** * **Lexoffice:** Online software for invoicing, accounting, banking and tax filing with document storage; **Service provider**: Haufe Service Center GmbH, Munzinger Straße 9, 79111 Freiburg, Germany; **Legal Basis:** Legitimate Interests (Article 6 (1) (f) GDPR); **Website:** [https://www.lexoffice.de](https://www.lexoffice.de); **Privacy Policy:** [https://www.lexoffice.de/datenschutz/](https://www.lexoffice.de/datenschutz/). **Data Processing Agreement:** [https://www.lexoffice.de/auftragsverarbeitung/](https://www.lexoffice.de/auftragsverarbeitung/). ## Provision of online services and web hosting -------------------------------------------- We process user data in order to be able to provide them with our online services. For this purpose, we process the IP address of the user, which is necessary to transmit the content and functions of our online services to the user's browser or terminal device. * **Processed data types:** Usage data (e.g. websites visited, interest in content, access times). Meta, communication and process data (e.g. IP addresses, time information, identification numbers, consent status). * **Data subjects:** Users (e.g. website visitors, users of online services). * **Purposes of Processing:** Provision of our online services and usability; Information technology infrastructure (Operation and provision of information systems and technical devices, such as computers, servers, etc.).). Security measures. * **Legal Basis:** Legitimate Interests (Article 6 (1) (f) GDPR). **Further information on processing methods, procedures and services used:** * **Collection of Access Data and Log Files:** The access to our online services is logged in the form of so-called "server log files". Server log files may include the address and name of the web pages and files accessed, the date and time of access, data volumes transferred, notification of successful access, browser type and version, the user's operating system, referrer URL (the previously visited page) and, as a general rule, IP addresses and the requesting provider. The server log files can be used for security purposes, e.g. to avoid overloading the servers (especially in the case of abusive attacks, so-called DDoS attacks) and to ensure the stability and optimal load balancing of the servers; **Legal Basis:** Legitimate Interests (Article 6 (1) (f) GDPR). **Retention period:** Log file information is stored for a maximum period of 30 days and then deleted or anonymized. Data, the further storage of which is necessary for evidence purposes, are excluded from deletion until the respective incident has been finally clarified. * **Netlify:** Creation, management and hosting of websites, online forms and other web elements; **Service provider**: Netlify, Inc, 2343 3rd Street, Suite 296, San Francisco, California 94107, USA; **Legal Basis:** Legitimate Interests (Article 6 (1) (f) GDPR); **Website:** [https://www.netlify.com/](https://www.netlify.com/); **Privacy Policy:** [https://www.netlify.com/privacy](https://www.netlify.com/privacy); **Data Processing Agreement:** [https://www.netlify.com/gdpr-ccpa/](https://www.netlify.com/gdpr-ccpa/). **Basis for third country transfer:** Standard Contractual Clauses ([https://www.netlify.com/gdpr-ccpa/](https://www.netlify.com/gdpr-ccpa/)). ## Contact and Inquiry Management ------------------------------ When contacting us (e.g. via mail, contact form, e-mail, telephone or via social media) as well as in the context of existing user and business relationships, the information of the inquiring persons is processed to the extent necessary to respond to the contact requests and any requested measures. * **Processed data types:** Contact data (e.g. e-mail, telephone numbers); Content data (e.g. text input, photographs, videos); Usage data (e.g. websites visited, interest in content, access times). Meta, communication and process data (e.g. IP addresses, time information, identification numbers, consent status). * **Data subjects:** Communication partner (Recipients of e-mails, letters, etc.). * **Purposes of Processing:** Contact requests and communication; Managing and responding to inquiries; Feedback (e.g. collecting feedback via online form). Provision of our online services and usability. * **Legal Basis:** Legitimate Interests (Article 6 (1) (f) GDPR). Performance of a contract and prior requests (Article 6 (1) (b) GDPR). **Further information on processing methods, procedures and services used:** * **Freshdesk:** Management of contact requests and communication; **Service provider**: Freshworks, Inc., 2950 S.Delaware Street, Suite 201, San Mateo, CA 94403, USA; **Legal Basis:** Performance of a contract and prior requests (Article 6 (1) (b) GDPR), Legitimate Interests (Article 6 (1) (f) GDPR); **Website:** [https://www.freshworks.com](https://www.freshworks.com); **Privacy Policy:** [https://www.freshworks.com/privacy/](https://www.freshworks.com/privacy/); **Data Processing Agreement:** [https://www.freshworks.com/data-processing-addendum/](https://www.freshworks.com/data-processing-addendum/). **Basis for third country transfer:** Standard Contractual Clauses ([https://www.freshworks.com/data-processing-addendum/](https://www.freshworks.com/data-processing-addendum/)). ## Cloud Services -------------- We use Internet-accessible software services (so-called "cloud services", also referred to as "Software as a Service") provided on the servers of its providers for the storage and management of content (e.g. document storage and management, exchange of documents, content and information with certain recipients or publication of content and information). Within this framework, personal data may be processed and stored on the provider's servers insofar as this data is part of communication processes with us or is otherwise processed by us in accordance with this privacy policy. This data may include in particular master data and contact data of data subjects, data on processes, contracts, other proceedings and their contents. Cloud service providers also process usage data and metadata that they use for security and service optimization purposes. If we use cloud services to provide documents and content to other users or publicly accessible websites, forms, etc., providers may store cookies on users' devices for web analysis or to remember user settings (e.g. in the case of media control). * **Processed data types:** Inventory data (e.g. names, addresses); Contact data (e.g. e-mail, telephone numbers); Content data (e.g. text input, photographs, videos); Usage data (e.g. websites visited, interest in content, access times). Meta, communication and process data (e.g. IP addresses, time information, identification numbers, consent status). * **Data subjects:** Customers; Employees (e.g. Employees, job applicants); Prospective customers; Communication partner (Recipients of e-mails, letters, etc.). Users (e.g. website visitors, users of online services). * **Purposes of Processing:** Office and organisational procedures; Information technology infrastructure (Operation and provision of information systems and technical devices, such as computers, servers, etc.).); Web Analytics (e.g. access statistics, recognition of returning visitors); Targeting (e.g. profiling based on interests and behaviour, use of cookies); Conversion tracking (Measurement of the effectiveness of marketing activities); Affiliate Tracking; Marketing; Profiles with user-related information (Creating user profiles). Provision of our online services and usability. * **Legal Basis:** Legitimate Interests (Article 6 (1) (f) GDPR). **Further information on processing methods, procedures and services used:** * **Supabase:** Cloud-based platform that provides developers with a set of tools for building and scaling applications, including authentication (secure way to add authentication to the application, with support for multiple authentication providers, passwordless sign-in, social login, and multi-factor authentication), real-time database, APIs (interfaces with built-in support for access control, filtering, sorting, and pagination as well as serverless functions), storage (file storage services in the cloud with support for object and relational storage, image resizing, and server-side rendering), and analytics (analysis services for measuring user behavior and application usage, with support for custom event tracking, cohort analysis, and user segmentation, as well as integration with other analytics platforms); **Service provider**: Supabase, Inc., 970 Toa Payoh North #07-04, Singapore 318992; **Legal Basis:** Legitimate Interests (Article 6 (1) (f) GDPR); **Website:** [https://supabase.com/](https://supabase.com/). **Privacy Policy:** [https://supabase.com/privacy](https://supabase.com/privacy). ## Newsletter and Electronic Communications ---------------------------------------- We send newsletters, e-mails and other electronic communications (hereinafter referred to as "newsletters") only with the consent of the recipient or a legal permission. Insofar as the contents of the newsletter are specifically described within the framework of registration, they are decisive for the consent of the user. Otherwise, our newsletters contain information about our services and us. In order to subscribe to our newsletters, it is generally sufficient to enter your e-mail address. We may, however, ask you to provide a name for the purpose of contacting you personally in the newsletter or to provide further information if this is required for the purposes of the newsletter. **Double opt-in procedure:** The registration to our newsletter takes place in general in a so-called Double-Opt-In procedure. This means that you will receive an e-mail after registration asking you to confirm your registration. This confirmation is necessary so that no one can register with external e-mail addresses. The registrations for the newsletter are logged in order to be able to prove the registration process according to the legal requirements. This includes storing the login and confirmation times as well as the IP address. Likewise the changes of your data stored with the dispatch service provider are logged. **Deletion and restriction of processing:** We may store the unsubscribed email addresses for up to three years based on our legitimate interests before deleting them to provide evidence of prior consent. The processing of these data is limited to the purpose of a possible defense against claims. An individual deletion request is possible at any time, provided that the former existence of a consent is confirmed at the same time. In the case of an obligation to permanently observe an objection, we reserve the right to store the e-mail address solely for this purpose in a blocklist. The logging of the registration process takes place on the basis of our legitimate interests for the purpose of proving its proper course. If we commission a service provider to send e-mails, this is done on the basis of our legitimate interests in an efficient and secure sending system. **Contents:** Information about us, our services, promotions and offers. * **Processed data types:** Inventory data (e.g. names, addresses); Contact data (e.g. e-mail, telephone numbers); Meta, communication and process data (e.g. IP addresses, time information, identification numbers, consent status). Usage data (e.g. websites visited, interest in content, access times). * **Data subjects:** Communication partner (Recipients of e-mails, letters, etc.). * **Purposes of Processing:** Direct marketing (e.g. by e-mail or postal). Web Analytics (e.g. access statistics, recognition of returning visitors). * **Legal Basis:** Consent (Article 6 (1) (a) GDPR). Legitimate Interests (Article 6 (1) (f) GDPR). * **Opt-Out:** You can cancel the receipt of our newsletter at any time, i.e. revoke your consent or object to further receipt. You will find a link to cancel the newsletter either at the end of each newsletter or you can otherwise use one of the contact options listed above, preferably e-mail. **Further information on processing methods, procedures and services used:** * **Brevo:** E-mail dispatch and automation services; **Service provider**: Sendinblue GmbH, Köpenicker Str. 126, 10179 Berlin, Germany; **Legal Basis:** Legitimate Interests (Article 6 (1) (f) GDPR); **Website:** [https://www.brevo.com/](https://www.brevo.com/); **Privacy Policy:** [https://www.brevo.com/de/legal/privacypolicy/](https://www.brevo.com/de/legal/privacypolicy/). **Data Processing Agreement:** Provided by the service provider. ## Web Analysis, Monitoring and Optimization ----------------------------------------- Web analysis is used to evaluate the visitor traffic on our website and may include the behaviour, interests or demographic information of users, such as age or gender, as pseudonymous values. With the help of web analysis we can e.g. recognize, at which time our online services or their functions or contents are most frequently used or requested for repeatedly, as well as which areas require optimization. In addition to web analysis, we can also use test procedures, e.g. to test and optimize different versions of our online services or their components. Unless otherwise stated below, profiles, i.e. data aggregated for a usage process, can be created for these purposes and information can be stored in a browser or in a terminal device and read from it. The information collected includes, in particular, websites visited and elements used there as well as technical information such as the browser used, the computer system used and information on usage times. If users have agreed to the collection of their location data from us or from the providers of the services we use, location data may also be processed. Unless otherwise stated below, profiles, that is data summarized for a usage process or user, may be created for these purposes and stored in a browser or terminal device (so-called "cookies") or similar processes may be used for the same purpose. The information collected includes, in particular, websites visited and elements used there as well as technical information such as the browser used, the computer system used and information on usage times. If users have consented to the collection of their location data or profiles to us or to the providers of the services we use, these may also be processed, depending on the provider. The IP addresses of the users are also stored. However, we use any existing IP masking procedure (i.e. pseudonymisation by shortening the IP address) to protect the user. In general, within the framework of web analysis, A/B testing and optimisation, no user data (such as e-mail addresses or names) is stored, but pseudonyms. This means that we, as well as the providers of the software used, do not know the actual identity of the users, but only the information stored in their profiles for the purposes of the respective processes. * **Processed data types:** Usage data (e.g. websites visited, interest in content, access times). Meta, communication and process data (e.g. IP addresses, time information, identification numbers, consent status). * **Data subjects:** Users (e.g. website visitors, users of online services). * **Purposes of Processing:** Web Analytics (e.g. access statistics, recognition of returning visitors). Profiles with user-related information (Creating user profiles). * **Security measures:** IP Masking (Pseudonymization of the IP address). * **Legal Basis:** Legitimate Interests (Article 6 (1) (f) GDPR). **Further information on processing methods, procedures and services used:** * **plausible.io:** Web Analytics; no use of cookies or comparable persistent online identifiers, the recognition of returning visitors is done with the help of a pseudonymous identifier, which is deleted after one day; apart from that, no personal data is stored ([https://plausible.io/data-policy](https://plausible.io/data-policy)); no data is passed on to third parties; the processing takes place on the server of plausible.io on the basis of an data processing agreement;; **Service provider**: Plausible Insights OÜ, Västriku tn 2, 50403, Tartu, Estonia; **Legal Basis:** Legitimate Interests (Article 6 (1) (f) GDPR); **Website:** [https://plausible.io/](https://plausible.io/); **Privacy Policy:** [https://plausible.io/privacy](https://plausible.io/privacy). **Data Processing Agreement:** [https://plausible.io/dpa](https://plausible.io/dpa). ## Changes and Updates to the Privacy Policy ----------------------------------------- We kindly ask you to inform yourself regularly about the contents of our data protection declaration. We will adjust the privacy policy as changes in our data processing practices make this necessary. We will inform you as soon as the changes require your cooperation (e.g. consent) or other individual notification. If we provide addresses and contact information of companies and organizations in this privacy policy, we ask you to note that addresses may change over time and to verify the information before contacting us. ---