diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c2e7f4..bcb02b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,9 @@ name: CI +# Runners: uvacompute (https://uvacompute.com) +# To enable, set the UVA_RUNNER repo variable to the correct runner label. +# runs-on: ${{ vars.UVA_RUNNER || 'ubuntu-latest' }} + on: pull_request: branches: [main] @@ -22,10 +26,7 @@ on: publish_crates: description: Publish to crates.io type: boolean - default: true - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + default: false permissions: contents: write @@ -33,7 +34,7 @@ permissions: jobs: changes: name: Changes - runs-on: [self-hosted, netty] + runs-on: ubuntu-latest outputs: rust: ${{ steps.check.outputs.rust }} version: ${{ steps.version.outputs.version }} @@ -101,7 +102,7 @@ jobs: name: Validate needs: changes if: needs.changes.outputs.rust == 'true' - runs-on: [self-hosted, netty] + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -125,6 +126,9 @@ jobs: - name: Install site dependencies run: pnpm --dir site install --frozen-lockfile + - name: Install system dependencies + run: sudo apt-get update && sudo apt-get install -y libx11-dev libxtst-dev + - name: Format check run: make fmt-check @@ -141,7 +145,7 @@ jobs: name: Integration (Xvfb) needs: changes if: needs.changes.outputs.rust == 'true' - runs-on: [self-hosted, netty] + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -149,6 +153,9 @@ jobs: - uses: Swatinem/rust-cache@v2 + - name: Install system dependencies + run: sudo apt-get update && sudo apt-get install -y libx11-dev libxtst-dev xvfb + - name: Xvfb integration tests run: make test-integration @@ -156,7 +163,7 @@ jobs: name: Distribution Validate needs: changes if: needs.changes.outputs.rust == 'true' - runs-on: [self-hosted, netty] + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -168,15 +175,49 @@ jobs: with: node-version: 22 + - uses: cachix/install-nix-action@v30 + with: + extra_nix_config: | + experimental-features = nix-command flakes + + - name: Install system dependencies + run: sudo apt-get update && sudo apt-get install -y libx11-dev libxtst-dev + - name: Distribution validation run: make dist-validate - # --- Release pipeline: update-manifests -> build -> release -> publish --- - # These stay on ubuntu-latest for artifact upload/download and registry publishing. + build: + name: Build Release Asset + needs: [changes, validate, integration, distribution] + if: github.event_name != 'pull_request' && needs.changes.outputs.rust == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + + - uses: Swatinem/rust-cache@v2 + + - name: Install system dependencies + run: sudo apt-get update && sudo apt-get install -y libx11-dev libxtst-dev + + - name: Clippy + run: cargo clippy -- -D warnings + + - name: Build + run: cargo build --release --locked + + - uses: actions/upload-artifact@v4 + with: + name: deskctl-linux-x86_64 + path: target/release/deskctl + retention-days: 7 update-manifests: name: Update Manifests - needs: [changes, validate, integration, distribution] + needs: [changes, build] if: github.event_name != 'pull_request' && needs.changes.outputs.rust == 'true' runs-on: ubuntu-latest steps: @@ -217,47 +258,6 @@ jobs: git tag "${{ needs.changes.outputs.tag }}" git push origin main --tags - build: - name: Build Release Asset - needs: [changes, update-manifests] - if: github.event_name != 'pull_request' && needs.changes.outputs.rust == 'true' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ needs.changes.outputs.tag }} - - - uses: dtolnay/rust-toolchain@stable - with: - components: clippy - - - uses: Swatinem/rust-cache@v2 - - - name: Install system dependencies - run: sudo apt-get update && sudo apt-get install -y libx11-dev libxtst-dev - - - name: Verify version - run: | - CARGO_VER=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/') - EXPECTED="${{ needs.changes.outputs.version }}" - if [ "$CARGO_VER" != "$EXPECTED" ]; then - echo "Version mismatch: Cargo.toml=$CARGO_VER expected=$EXPECTED" - exit 1 - fi - echo "Building version $CARGO_VER" - - - name: Clippy - run: cargo clippy -- -D warnings - - - name: Build - run: cargo build --release --locked - - - uses: actions/upload-artifact@v4 - with: - name: deskctl-linux-x86_64 - path: target/release/deskctl - retention-days: 7 - release: name: Release needs: [changes, build, update-manifests] @@ -291,51 +291,10 @@ jobs: artifacts/checksums.txt fi - publish-npm: - name: Publish npm + publish: + name: Publish needs: [changes, update-manifests, release] - if: >- - github.event_name != 'pull_request' - && needs.changes.outputs.rust == 'true' - && (inputs.publish_npm == true || inputs.publish_npm == '') - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ needs.changes.outputs.tag }} - - - uses: actions/setup-node@v4 - with: - node-version: 22 - registry-url: https://registry.npmjs.org - - - name: Check if already published - id: published - run: | - VERSION="${{ needs.changes.outputs.version }}" - if npm view "deskctl@${VERSION}" version >/dev/null 2>&1; then - echo "npm=true" >> "$GITHUB_OUTPUT" - else - echo "npm=false" >> "$GITHUB_OUTPUT" - fi - - - name: Validate npm package - if: steps.published.outputs.npm != 'true' - run: node npm/deskctl/scripts/validate-package.js - - - name: Publish npm - if: steps.published.outputs.npm != 'true' - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - run: npm publish ./npm/deskctl --access public - - publish-crates: - name: Publish crates.io - needs: [changes, update-manifests, release] - if: >- - github.event_name != 'pull_request' - && needs.changes.outputs.rust == 'true' - && (inputs.publish_crates == true || inputs.publish_crates == '') + if: github.event_name != 'pull_request' && needs.changes.outputs.rust == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -344,21 +303,40 @@ jobs: - uses: dtolnay/rust-toolchain@stable + - uses: actions/setup-node@v4 + with: + node-version: 22 + registry-url: https://registry.npmjs.org + - name: Install system dependencies run: sudo apt-get update && sudo apt-get install -y libx11-dev libxtst-dev - - name: Check if already published + - name: Check current published state id: published run: | VERSION="${{ needs.changes.outputs.version }}" + if npm view "deskctl@${VERSION}" version >/dev/null 2>&1; then + echo "npm=true" >> "$GITHUB_OUTPUT" + else + echo "npm=false" >> "$GITHUB_OUTPUT" + fi if curl -fsSL "https://crates.io/api/v1/crates/deskctl/${VERSION}" >/dev/null 2>&1; then echo "crates=true" >> "$GITHUB_OUTPUT" else echo "crates=false" >> "$GITHUB_OUTPUT" fi + - name: Validate npm package + run: node npm/deskctl/scripts/validate-package.js + + - name: Publish npm + if: steps.published.outputs.npm != 'true' + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm publish ./npm/deskctl --access public + - name: Publish crates.io - if: steps.published.outputs.crates != 'true' + if: inputs.publish_crates && steps.published.outputs.crates != 'true' env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} run: cargo publish --locked diff --git a/Cargo.lock b/Cargo.lock index eb0e2ce..9680966 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -241,9 +241,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.58" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "jobserver", @@ -400,7 +400,7 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "deskctl" -version = "0.1.14" +version = "0.1.10" dependencies = [ "ab_glyph", "anyhow", @@ -911,9 +911,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.92" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4c90f45aa2e6eacbe8645f77fdea542ac97a494bcd117a67df9ff4d611f995" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -1039,9 +1039,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "wasi", @@ -1699,9 +1699,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.9" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "simd_helpers" @@ -1861,9 +1861,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.0" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -1907,9 +1907,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.115" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6523d69017b7633e396a89c5efab138161ed5aafcbc8d3e5c5a42ae38f50495a" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -1920,9 +1920,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.115" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3a6c758eb2f701ed3d052ff5737f5bfe6614326ea7f3bbac7156192dc32e67" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1930,9 +1930,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.115" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "921de2737904886b52bcbb237301552d05969a6f9c40d261eb0533c8b055fedf" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -1943,9 +1943,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.115" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a93e946af942b58934c604527337bad9ae33ba1d5c6900bbb41c2c07c2364a93" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] @@ -2297,9 +2297,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.5.15" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +checksum = "0b7a1c0af6e5d8d1363f4994b7a091ccf963d8b694f7da5b0b9cceb82da2c0a6" dependencies = [ "zune-core", ] diff --git a/Cargo.toml b/Cargo.toml index be051c7..cc6d11a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deskctl" -version = "0.1.14" +version = "0.1.10" edition = "2021" description = "X11 desktop control CLI for agents" license = "MIT" diff --git a/README.md b/README.md index dccbe04..935f329 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,9 @@ # deskctl + [![npm](https://img.shields.io/npm/v/deskctl?label=npm)](https://www.npmjs.com/package/deskctl) [![skill](https://img.shields.io/badge/skills.sh-deskctl-111827)](skills/deskctl) -Desktop control cli for AI agents on X11. - -https://github.com/user-attachments/assets/e820787e-4d1a-463f-bdcf-a829588778bf - +Desktop control cli for AI agents on Linux X11. ## Install diff --git a/demo/index.html b/demo/index.html deleted file mode 100644 index 70ac230..0000000 --- a/demo/index.html +++ /dev/null @@ -1,969 +0,0 @@ - - - - - -deskctl - Desktop Control for AI Agents - - - - -
-

deskctl

-

desktop control CLI for AI agents

-
- -
-
-
-
-
-
-
-
-
- - -
-
-
- Files ~/reports -
-
-
-
- 📝 - task_brief.txt - 2.1 KB -
-
- 📊 - nvda_q1_data.csv - 48 KB -
-
- 📄 - prev_report.pdf - 1.2 MB -
-
- 📁 - archive/ - -- -
-
-
- task: Prepare NVDA Q1 earnings summary
- source: finance.yahoo.com, local csv
- output: Google Docs report with chart -
-
-
- - -
-
-
- Chrome - Yahoo Finance -
-
-
- NVDA - $924.68 - +3.42% - 1Y -
-
- - - - - - - - - - - - - $950 - $800 - $650 - -
-
-
-
- - -
-
-
- Chrome - Google Docs -
-
-
-
-
-
-
-
-
-
-
-
-
-
- - - - - - - - - - - NVDA 1Y - -
-
-
-
- - -
@w1
-
@w2
-
@w3
- -
- -
- -
-
- - -
-
Files
-
Yahoo Finance
-
Google Docs
-
-
-
- -
-
-
-
-
- agent computer -
-
-
-
- -
-

AI agent controlling a live desktop via deskctl

- -
- - - - diff --git a/docs/releasing.md b/docs/releasing.md index 849d661..8f39d3f 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -59,12 +59,12 @@ The repository release workflow: - publishes the canonical GitHub Release asset - uploads `checksums.txt` -The registry publish jobs (npm and crates.io run in parallel): +The registry publish workflow: -- target an existing release tag -- check whether that version is already published on the respective registry -- skip already-published versions -- both default to enabled; can be toggled via workflow_dispatch inputs +- targets an existing release tag +- checks that Cargo, npm, and the requested tag all agree on version +- checks whether that version is already published on npm and crates.io +- only publishes the channels explicitly requested ## Rerun Safety diff --git a/npm/deskctl/package.json b/npm/deskctl/package.json index c676924..adb142c 100644 --- a/npm/deskctl/package.json +++ b/npm/deskctl/package.json @@ -1,6 +1,6 @@ { "name": "deskctl", - "version": "0.1.14", + "version": "0.1.10", "description": "Installable deskctl package for Linux X11 agents", "license": "MIT", "homepage": "https://github.com/harivansh-afk/deskctl", diff --git a/site/src/pages/commands.mdx b/site/src/pages/commands.mdx index 0696558..934cdb8 100644 --- a/site/src/pages/commands.mdx +++ b/site/src/pages/commands.mdx @@ -37,9 +37,9 @@ preferred read surface for focused state queries. ## Wait for state transitions ```sh -deskctl wait window --selector 'title=Chromium' --timeout 10 +deskctl wait window --selector 'title=Firefox' --timeout 10 deskctl wait focus --selector 'id=win3' --timeout 5 -deskctl --json wait window --selector 'class=chromium' --poll-ms 100 +deskctl --json wait window --selector 'class=firefox' --poll-ms 100 ``` Wait commands return the matched window payload on success. In `--json` mode, @@ -48,9 +48,9 @@ timeouts and selector failures expose structured `kind` values. ## Act on windows ```sh -deskctl launch chromium +deskctl launch firefox deskctl focus @w1 -deskctl focus 'title=Chromium' +deskctl focus 'title=Firefox' deskctl click @w1 deskctl click 960,540 deskctl dblclick @w2 @@ -86,8 +86,8 @@ more deterministic for automation, and easier to retry safely. ```sh ref=w1 id=win1 -title=Chromium -class=chromium +title=Firefox +class=firefox focused ``` @@ -99,7 +99,7 @@ w1 win1 ``` -Bare strings like `chromium` are fuzzy matches. They resolve when there is one +Bare strings like `firefox` are fuzzy matches. They resolve when there is one match and fail with candidate windows when there are multiple matches. ## Global options diff --git a/site/src/pages/index.astro b/site/src/pages/index.astro index 478c7a2..b914e16 100644 --- a/site/src/pages/index.astro +++ b/site/src/pages/index.astro @@ -43,9 +43,6 @@ import DocLayout from "../layouts/DocLayout.astro";
  • GitHub
  • -
  • - crates.io -
  • npm
  • diff --git a/site/src/pages/quick-start.mdx b/site/src/pages/quick-start.mdx index 4cc0e25..7ecf5a7 100644 --- a/site/src/pages/quick-start.mdx +++ b/site/src/pages/quick-start.mdx @@ -38,13 +38,13 @@ Prefer explicit selectors when you need deterministic targeting: ```sh ref=w1 id=win1 -title=Chromium -class=chromium +title=Firefox +class=firefox focused ``` Legacy refs such as `@w1` still work after `snapshot` or `list-windows`. Bare -strings like `chromium` are fuzzy matches and now fail on ambiguity. +strings like `firefox` are fuzzy matches and now fail on ambiguity. ## 4. Wait, act, verify @@ -55,16 +55,16 @@ The core loop is: deskctl snapshot --annotate # wait -deskctl wait window --selector 'title=Chromium' --timeout 10 +deskctl wait window --selector 'title=Firefox' --timeout 10 # act -deskctl focus 'title=Chromium' +deskctl focus 'title=Firefox' deskctl hotkey ctrl l deskctl type "https://example.com" deskctl press enter # verify -deskctl wait focus --selector 'title=Chromium' --timeout 5 +deskctl wait focus --selector 'title=Firefox' --timeout 5 deskctl snapshot ``` @@ -84,8 +84,8 @@ Every command supports `--json` and uses the same top-level envelope: { "ref_id": "w1", "window_id": "win1", - "title": "Chromium", - "app_name": "chromium", + "title": "Firefox", + "app_name": "firefox", "x": 0, "y": 0, "width": 1920, diff --git a/skills/deskctl/SKILL.md b/skills/deskctl/SKILL.md index c79ca21..67a77c5 100644 --- a/skills/deskctl/SKILL.md +++ b/skills/deskctl/SKILL.md @@ -30,8 +30,8 @@ Every desktop interaction follows: **observe -> wait -> act -> verify**. ```bash deskctl snapshot --annotate # observe -deskctl wait window --selector 'title=Chromium' --timeout 10 # wait -deskctl click 'title=Chromium' # act +deskctl wait window --selector 'title=Firefox' --timeout 10 # wait +deskctl click 'title=Firefox' # act deskctl snapshot # verify ``` @@ -42,12 +42,12 @@ See [workflows/observe-act.sh](workflows/observe-act.sh) for a reusable script. ```bash ref=w1 # snapshot ref (short-lived) id=win1 # stable window ID (session-scoped) -title=Chromium # match by title -class=chromium # match by WM class +title=Firefox # match by title +class=firefox # match by WM class focused # currently focused window ``` -Bare strings like `chromium` do fuzzy matching but fail on ambiguity. Prefer explicit selectors. +Bare strings like `firefox` do fuzzy matching but fail on ambiguity. Prefer explicit selectors. ## References diff --git a/skills/deskctl/references/commands.md b/skills/deskctl/references/commands.md index df69350..27b4310 100644 --- a/skills/deskctl/references/commands.md +++ b/skills/deskctl/references/commands.md @@ -23,8 +23,8 @@ deskctl get-mouse-position ## Wait ```bash -deskctl wait window --selector 'title=Chromium' --timeout 10 -deskctl wait focus --selector 'class=chromium' --timeout 5 +deskctl wait window --selector 'title=Firefox' --timeout 10 +deskctl wait focus --selector 'class=firefox' --timeout 5 ``` Returns the matched window payload on success. Failures include structured @@ -35,8 +35,8 @@ Returns the matched window payload on success. Failures include structured ```bash ref=w1 id=win1 -title=Chromium -class=chromium +title=Firefox +class=firefox focused ``` @@ -46,7 +46,7 @@ on ambiguity. ## Act ```bash -deskctl focus 'class=chromium' +deskctl focus 'class=firefox' deskctl click @w1 deskctl dblclick @w2 deskctl type "hello world" @@ -59,7 +59,7 @@ deskctl mouse drag 100 100 500 500 deskctl move-window @w1 100 120 deskctl resize-window @w1 1280 720 deskctl close @w3 -deskctl launch chromium +deskctl launch firefox ``` The daemon starts automatically on first command. In normal usage you should diff --git a/skills/deskctl/workflows/observe-act.sh b/skills/deskctl/workflows/observe-act.sh index 8c3abc2..0e336ae 100755 --- a/skills/deskctl/workflows/observe-act.sh +++ b/skills/deskctl/workflows/observe-act.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # observe-act.sh - main desktop interaction loop # usage: ./observe-act.sh [action] [action-args...] -# example: ./observe-act.sh 'title=Chromium' click +# example: ./observe-act.sh 'title=Firefox' click # example: ./observe-act.sh 'class=terminal' type "ls -la" set -euo pipefail diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 79008de..28092d7 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -48,13 +48,13 @@ pub enum Command { /// Click a window ref or coordinates #[command(after_help = CLICK_EXAMPLES)] Click { - /// Selector (ref=w1, id=win1, title=Chromium, class=chromium, focused) or x,y coordinates + /// Selector (ref=w1, id=win1, title=Firefox, class=firefox, focused) or x,y coordinates selector: String, }, /// Double-click a window ref or coordinates #[command(after_help = DBLCLICK_EXAMPLES)] Dblclick { - /// Selector (ref=w1, id=win1, title=Chromium, class=chromium, focused) or x,y coordinates + /// Selector (ref=w1, id=win1, title=Firefox, class=firefox, focused) or x,y coordinates selector: String, }, /// Type text into the focused window @@ -81,19 +81,19 @@ pub enum Command { /// Focus a window by ref or name #[command(after_help = FOCUS_EXAMPLES)] Focus { - /// Selector: ref=w1, id=win1, title=Chromium, class=chromium, focused, or a fuzzy substring + /// Selector: ref=w1, id=win1, title=Firefox, class=firefox, focused, or a fuzzy substring selector: String, }, /// Close a window by ref or name #[command(after_help = CLOSE_EXAMPLES)] Close { - /// Selector: ref=w1, id=win1, title=Chromium, class=chromium, focused, or a fuzzy substring + /// Selector: ref=w1, id=win1, title=Firefox, class=firefox, focused, or a fuzzy substring selector: String, }, /// Move a window #[command(after_help = MOVE_WINDOW_EXAMPLES)] MoveWindow { - /// Selector: ref=w1, id=win1, title=Chromium, class=chromium, focused, or a fuzzy substring + /// Selector: ref=w1, id=win1, title=Firefox, class=firefox, focused, or a fuzzy substring selector: String, /// X position x: i32, @@ -103,7 +103,7 @@ pub enum Command { /// Resize a window #[command(after_help = RESIZE_WINDOW_EXAMPLES)] ResizeWindow { - /// Selector: ref=w1, id=win1, title=Chromium, class=chromium, focused, or a fuzzy substring + /// Selector: ref=w1, id=win1, title=Firefox, class=firefox, focused, or a fuzzy substring selector: String, /// Width w: u32, @@ -210,19 +210,19 @@ const SNAPSHOT_EXAMPLES: &str = const LIST_WINDOWS_EXAMPLES: &str = "Examples:\n deskctl list-windows\n deskctl --json list-windows"; const CLICK_EXAMPLES: &str = - "Examples:\n deskctl click @w1\n deskctl click 'title=Chromium'\n deskctl click 500,300"; + "Examples:\n deskctl click @w1\n deskctl click 'title=Firefox'\n deskctl click 500,300"; const DBLCLICK_EXAMPLES: &str = - "Examples:\n deskctl dblclick @w2\n deskctl dblclick 'class=chromium'\n deskctl dblclick 500,300"; + "Examples:\n deskctl dblclick @w2\n deskctl dblclick 'class=firefox'\n deskctl dblclick 500,300"; const TYPE_EXAMPLES: &str = "Examples:\n deskctl type \"hello world\"\n deskctl type \"https://example.com\""; const PRESS_EXAMPLES: &str = "Examples:\n deskctl press enter\n deskctl press escape"; const HOTKEY_EXAMPLES: &str = "Examples:\n deskctl hotkey ctrl l\n deskctl hotkey ctrl shift t"; const FOCUS_EXAMPLES: &str = - "Examples:\n deskctl focus @w1\n deskctl focus 'title=Chromium'\n deskctl focus focused"; + "Examples:\n deskctl focus @w1\n deskctl focus 'title=Firefox'\n deskctl focus focused"; const CLOSE_EXAMPLES: &str = - "Examples:\n deskctl close @w3\n deskctl close 'id=win2'\n deskctl close 'class=chromium'"; + "Examples:\n deskctl close @w3\n deskctl close 'id=win2'\n deskctl close 'class=firefox'"; const MOVE_WINDOW_EXAMPLES: &str = - "Examples:\n deskctl move-window @w1 100 200\n deskctl move-window 'title=Chromium' 0 0"; + "Examples:\n deskctl move-window @w1 100 200\n deskctl move-window 'title=Firefox' 0 0"; const RESIZE_WINDOW_EXAMPLES: &str = "Examples:\n deskctl resize-window @w1 1280 720\n deskctl resize-window 'id=win2' 800 600"; const GET_MONITORS_EXAMPLES: &str = @@ -237,12 +237,12 @@ const GET_MOUSE_POSITION_EXAMPLES: &str = const DOCTOR_EXAMPLES: &str = "Examples:\n deskctl doctor\n deskctl --json doctor"; const UPGRADE_EXAMPLES: &str = "Examples:\n deskctl upgrade\n deskctl upgrade --yes\n deskctl --json upgrade --yes"; -const WAIT_WINDOW_EXAMPLES: &str = "Examples:\n deskctl wait window --selector 'title=Chromium' --timeout 10\n deskctl --json wait window --selector 'class=chromium' --poll-ms 100"; +const WAIT_WINDOW_EXAMPLES: &str = "Examples:\n deskctl wait window --selector 'title=Firefox' --timeout 10\n deskctl --json wait window --selector 'class=firefox' --poll-ms 100"; const WAIT_FOCUS_EXAMPLES: &str = "Examples:\n deskctl wait focus --selector 'id=win3' --timeout 5\n deskctl wait focus --selector focused --poll-ms 200"; const SCREENSHOT_EXAMPLES: &str = "Examples:\n deskctl screenshot\n deskctl screenshot /tmp/screen.png\n deskctl screenshot --annotate"; const LAUNCH_EXAMPLES: &str = - "Examples:\n deskctl launch chromium\n deskctl launch code -- --new-window"; + "Examples:\n deskctl launch firefox\n deskctl launch code -- --new-window"; const MOUSE_MOVE_EXAMPLES: &str = "Examples:\n deskctl mouse move 500 300\n deskctl mouse move 0 0"; const MOUSE_SCROLL_EXAMPLES: &str = @@ -277,7 +277,7 @@ pub enum WaitCmd { #[derive(Args)] pub struct WaitSelectorOpts { - /// Selector: ref=w1, id=win1, title=Chromium, class=chromium, focused, or a fuzzy substring + /// Selector: ref=w1, id=win1, title=Firefox, class=firefox, focused, or a fuzzy substring #[arg(long)] pub selector: String, @@ -1103,8 +1103,8 @@ mod tests { "windows": [{ "ref_id": "w1", "window_id": "win1", - "title": "Chromium", - "app_name": "chromium", + "title": "Firefox", + "app_name": "firefox", "x": 0, "y": 0, "width": 1280, @@ -1125,37 +1125,37 @@ mod tests { fn action_text_includes_target_identity() { let lines = render_success_lines( &Command::Focus { - selector: "title=Chromium".to_string(), + selector: "title=Firefox".to_string(), }, Some(&json!({ "action": "focus", - "window": "Chromium", - "title": "Chromium", + "window": "Firefox", + "title": "Firefox", "ref_id": "w2", "window_id": "win7" })), ) .unwrap(); - assert_eq!(lines, vec!["Focused @w2 [win7] \"Chromium\""]); + assert_eq!(lines, vec!["Focused @w2 [win7] \"Firefox\""]); } #[test] fn timeout_errors_render_last_observation() { let lines = render_error_lines(&Response::err_with_data( - "Timed out waiting for focus to match selector: title=Chromium", + "Timed out waiting for focus to match selector: title=Firefox", json!({ "kind": "timeout", "wait": "focus", - "selector": "title=Chromium", + "selector": "title=Firefox", "timeout_ms": 1000, "last_observation": { "kind": "window_not_focused", "window": { "ref_id": "w1", "window_id": "win1", - "title": "Chromium", - "app_name": "chromium", + "title": "Firefox", + "app_name": "firefox", "x": 0, "y": 0, "width": 1280, @@ -1167,8 +1167,10 @@ mod tests { }), )); - assert!(lines.iter().any(|line| line - .contains("Timed out after 1000ms waiting for focus selector title=Chromium"))); + assert!(lines + .iter() + .any(|line| line + .contains("Timed out after 1000ms waiting for focus selector title=Firefox"))); assert!(lines .iter() .any(|line| line.contains("matching window exists but is not focused yet"))); @@ -1188,9 +1190,9 @@ mod tests { let summary = target_summary(&json!({ "ref_id": "w1", "window_id": "win1", - "title": "Chromium" + "title": "Firefox" })); - assert_eq!(summary.as_deref(), Some("@w1 [win1] \"Chromium\"")); + assert_eq!(summary.as_deref(), Some("@w1 [win1] \"Firefox\"")); } #[test] diff --git a/src/core/refs.rs b/src/core/refs.rs index 7fd7b6c..34e1ba7 100644 --- a/src/core/refs.rs +++ b/src/core/refs.rs @@ -412,8 +412,8 @@ mod tests { SelectorQuery::WindowId("win4".to_string()) ); assert_eq!( - SelectorQuery::parse("title=Chromium"), - SelectorQuery::Title("Chromium".to_string()) + SelectorQuery::parse("title=Firefox"), + SelectorQuery::Title("Firefox".to_string()) ); assert_eq!( SelectorQuery::parse("class=Navigator"), @@ -458,11 +458,11 @@ mod tests { fn fuzzy_resolution_fails_with_candidates_when_ambiguous() { let mut refs = RefMap::new(); refs.rebuild(&[ - sample_window(1, "Chromium"), + sample_window(1, "Firefox"), BackendWindow { native_id: 2, - title: "Chromium Settings".to_string(), - app_name: "Chromium".to_string(), + title: "Firefox Settings".to_string(), + app_name: "Firefox".to_string(), x: 0, y: 0, width: 10, @@ -472,7 +472,7 @@ mod tests { }, ]); - match refs.resolve("chromium") { + match refs.resolve("firefox") { ResolveResult::Ambiguous { mode, candidates, .. } => { diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs index 9e7e931..3df1d9a 100644 --- a/src/daemon/mod.rs +++ b/src/daemon/mod.rs @@ -1,7 +1,6 @@ mod handler; mod state; -use std::path::{Path, PathBuf}; use std::sync::Arc; use anyhow::{Context, Result}; @@ -13,29 +12,6 @@ use crate::core::paths::{pid_path_from_env, socket_path_from_env}; use crate::core::session; use state::DaemonState; -struct RuntimePathsGuard { - socket_path: PathBuf, - pid_path: Option, -} - -impl RuntimePathsGuard { - fn new(socket_path: PathBuf, pid_path: Option) -> Self { - Self { - socket_path, - pid_path, - } - } -} - -impl Drop for RuntimePathsGuard { - fn drop(&mut self) { - remove_runtime_path(&self.socket_path); - if let Some(ref pid_path) = self.pid_path { - remove_runtime_path(pid_path); - } - } -} - pub fn run() -> Result<()> { // Validate session before starting session::detect_session()?; @@ -49,6 +25,7 @@ pub fn run() -> Result<()> { async fn async_run() -> Result<()> { let socket_path = socket_path_from_env().context("DESKCTL_SOCKET_PATH not set")?; + let pid_path = pid_path_from_env(); // Clean up stale socket @@ -56,21 +33,20 @@ async fn async_run() -> Result<()> { std::fs::remove_file(&socket_path)?; } + // Write PID file + if let Some(ref pid_path) = pid_path { + std::fs::write(pid_path, std::process::id().to_string())?; + } + + let listener = UnixListener::bind(&socket_path) + .context(format!("Failed to bind socket: {}", socket_path.display()))?; + let session = std::env::var("DESKCTL_SESSION").unwrap_or_else(|_| "default".to_string()); let state = Arc::new(Mutex::new( DaemonState::new(session, socket_path.clone()) .context("Failed to initialize daemon state")?, )); - let listener = UnixListener::bind(&socket_path) - .context(format!("Failed to bind socket: {}", socket_path.display()))?; - let _runtime_paths = RuntimePathsGuard::new(socket_path.clone(), pid_path.clone()); - - // Write PID file only after the daemon is ready to serve requests. - if let Some(ref pid_path) = pid_path { - std::fs::write(pid_path, std::process::id().to_string())?; - } - let shutdown = Arc::new(tokio::sync::Notify::new()); let shutdown_clone = shutdown.clone(); @@ -99,6 +75,14 @@ async fn async_run() -> Result<()> { } } + // Cleanup + if socket_path.exists() { + let _ = std::fs::remove_file(&socket_path); + } + if let Some(ref pid_path) = pid_path { + let _ = std::fs::remove_file(pid_path); + } + Ok(()) } @@ -139,11 +123,3 @@ async fn handle_connection( Ok(()) } - -fn remove_runtime_path(path: &Path) { - if let Err(error) = std::fs::remove_file(path) { - if error.kind() != std::io::ErrorKind::NotFound { - eprintln!("Failed to remove runtime path {}: {error}", path.display()); - } - } -} diff --git a/tests/support/mod.rs b/tests/support/mod.rs index 07cc5a7..5c6f0be 100644 --- a/tests/support/mod.rs +++ b/tests/support/mod.rs @@ -4,7 +4,6 @@ use std::os::unix::net::UnixListener; use std::path::{Path, PathBuf}; use std::process::{Command, Output}; use std::sync::{Mutex, OnceLock}; -use std::thread; use std::time::{SystemTime, UNIX_EPOCH}; use anyhow::{anyhow, bail, Context, Result}; @@ -61,7 +60,8 @@ pub struct FixtureWindow { impl FixtureWindow { pub fn create(title: &str, app_class: &str) -> Result { - let (conn, screen_num) = connect_to_test_display()?; + let (conn, screen_num) = + x11rb::connect(None).context("Failed to connect to the integration test display")?; let screen = &conn.setup().roots[screen_num]; let window = conn.generate_id()?; @@ -103,26 +103,6 @@ impl FixtureWindow { } } -fn connect_to_test_display() -> Result<(RustConnection, usize)> { - let max_attempts = 10; - let mut last_error = None; - - for attempt in 0..max_attempts { - match x11rb::connect(None) { - Ok(connection) => return Ok(connection), - Err(error) => { - last_error = Some(anyhow!(error)); - if attempt + 1 < max_attempts { - thread::sleep(std::time::Duration::from_millis(100 * (attempt + 1) as u64)); - } - } - } - } - - Err(last_error.expect("x11 connection attempts should capture an error")) - .context("Failed to connect to the integration test display") -} - impl Drop for FixtureWindow { fn drop(&mut self) { let _ = self.conn.destroy_window(self.window); @@ -162,10 +142,6 @@ impl TestSession { .expect("TestSession always has an explicit socket path") } - pub fn pid_path(&self) -> PathBuf { - self.root.join("deskctl.pid") - } - pub fn create_stale_socket(&self) -> Result<()> { let listener = UnixListener::bind(self.socket_path()) .with_context(|| format!("Failed to bind {}", self.socket_path().display()))?; @@ -211,29 +187,6 @@ impl TestSession { ) }) } - - pub fn run_daemon(&self, env: I) -> Result - where - I: IntoIterator, - K: AsRef, - V: AsRef, - { - let mut command = Command::new(env!("CARGO_BIN_EXE_deskctl")); - command - .env("DESKCTL_DAEMON", "1") - .env("DESKCTL_SOCKET_PATH", self.socket_path()) - .env("DESKCTL_PID_PATH", self.pid_path()) - .env("DESKCTL_SESSION", &self.opts.session) - .envs(env); - - command.output().with_context(|| { - format!( - "Failed to run daemon {} against {}", - env!("CARGO_BIN_EXE_deskctl"), - self.socket_path().display() - ) - }) - } } impl Drop for TestSession { @@ -242,9 +195,6 @@ impl Drop for TestSession { if self.socket_path().exists() { let _ = std::fs::remove_file(self.socket_path()); } - if self.pid_path().exists() { - let _ = std::fs::remove_file(self.pid_path()); - } let _ = std::fs::remove_dir_all(&self.root); } } diff --git a/tests/x11_runtime.rs b/tests/x11_runtime.rs index 30308cb..2aac58c 100644 --- a/tests/x11_runtime.rs +++ b/tests/x11_runtime.rs @@ -114,31 +114,6 @@ fn daemon_start_recovers_from_stale_socket() -> Result<()> { Ok(()) } -#[test] -fn daemon_init_failure_cleans_runtime_state() -> Result<()> { - let _guard = env_lock_guard(); - let session = TestSession::new("daemon-init-failure")?; - - let output = session.run_daemon([("XDG_SESSION_TYPE", "x11"), ("DISPLAY", ":99999")])?; - assert!(!output.status.success(), "daemon startup should fail"); - - let stderr = String::from_utf8_lossy(&output.stderr); - assert!( - stderr.contains("Failed to initialize daemon state"), - "unexpected stderr: {stderr}" - ); - assert!( - !session.socket_path().exists(), - "failed startup should remove the socket path" - ); - assert!( - !session.pid_path().exists(), - "failed startup should remove the pid path" - ); - - Ok(()) -} - #[test] fn wait_window_returns_matched_window_payload() -> Result<()> { let _guard = env_lock_guard();