From 6af192c20dd7331e80bea0e8dea5eceb1aa43331 Mon Sep 17 00:00:00 2001 From: Quentin Eude Date: Wed, 11 Nov 2026 11:59:30 +0100 Subject: [PATCH 01/23] feat(contrib): add a Raycast extension Search windows and workspaces and run argument-free Defi commands from Raycast. The extension only runs the defi CLI with execFile and makes no network calls. --- README.md | 5 +- contrib/raycast/.gitignore | 2 + contrib/raycast/.prettierrc | 4 + contrib/raycast/README.md | 36 + contrib/raycast/assets/icon.png | Bin 0 -> 131469 bytes contrib/raycast/assets/icon@dark.png | Bin 0 -> 139021 bytes contrib/raycast/eslint.config.mjs | 4 + contrib/raycast/package-lock.json | 3592 +++++++++++++++++ contrib/raycast/package.json | 65 + contrib/raycast/src/defi.ts | 50 + contrib/raycast/src/parse.ts | 68 + contrib/raycast/src/run-command.tsx | 114 + contrib/raycast/src/search-windows.tsx | 64 + contrib/raycast/src/search-workspaces.tsx | 54 + .../raycast/test/fixtures/list-windows.json | 209 + .../test/fixtures/list-workspaces.json | 68 + contrib/raycast/test/parse.test.ts | 54 + contrib/raycast/tsconfig.json | 16 + 18 files changed, 4403 insertions(+), 2 deletions(-) create mode 100644 contrib/raycast/.gitignore create mode 100644 contrib/raycast/.prettierrc create mode 100644 contrib/raycast/README.md create mode 100644 contrib/raycast/assets/icon.png create mode 100644 contrib/raycast/assets/icon@dark.png create mode 100644 contrib/raycast/eslint.config.mjs create mode 100644 contrib/raycast/package-lock.json create mode 100644 contrib/raycast/package.json create mode 100644 contrib/raycast/src/defi.ts create mode 100644 contrib/raycast/src/parse.ts create mode 100644 contrib/raycast/src/run-command.tsx create mode 100644 contrib/raycast/src/search-windows.tsx create mode 100644 contrib/raycast/src/search-workspaces.tsx create mode 100644 contrib/raycast/test/fixtures/list-windows.json create mode 100644 contrib/raycast/test/fixtures/list-workspaces.json create mode 100644 contrib/raycast/test/parse.test.ts create mode 100644 contrib/raycast/tsconfig.json diff --git a/README.md b/README.md index 6052daa8..ec544a45 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,9 @@ Defi is in alpha, so behavior and configuration may still change. - **Smooth motion**: windows glide on an overlay, then move once underneath. - **App rules**: send apps to a workspace, float them, or set their width. - **Keyboard and mouse**: custom shortcuts with a cheatsheet, and drag to reorder. -- **Scriptable**: live TOML config, a CLI, and a - [SketchyBar integration](contrib/sketchybar/README.md). +- **Scriptable**: live TOML config, a CLI, a + [SketchyBar integration](contrib/sketchybar/README.md), and a + [Raycast extension](contrib/raycast/README.md). Window borders, gaps, and every other option are in the [configuration reference](CONFIGURATION.md). diff --git a/contrib/raycast/.gitignore b/contrib/raycast/.gitignore new file mode 100644 index 00000000..b6fce099 --- /dev/null +++ b/contrib/raycast/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +raycast-env.d.ts diff --git a/contrib/raycast/.prettierrc b/contrib/raycast/.prettierrc new file mode 100644 index 00000000..fc0f5030 --- /dev/null +++ b/contrib/raycast/.prettierrc @@ -0,0 +1,4 @@ +{ + "printWidth": 120, + "singleQuote": false +} diff --git a/contrib/raycast/README.md b/contrib/raycast/README.md new file mode 100644 index 00000000..e046ed5a --- /dev/null +++ b/contrib/raycast/README.md @@ -0,0 +1,36 @@ +# Raycast extension + +Reach Defi's windows, workspaces, and commands from +[Raycast](https://www.raycast.com). The extension only runs the `defi` CLI. It +makes no network calls, so window titles stay on your Mac. + +## Install + +You need Node.js 22 or later. From the Defi repository root: + +```sh +cd contrib/raycast +npm install && npm run dev +``` + +`npm run dev` adds the extension to Raycast in development mode. + +## Commands + +- **Search Windows** lists Defi's managed windows with their app, workspace, and + display. Enter focuses the window. **Move to Workspace** sends it to another + workspace and follows it there. +- **Search Workspaces** lists the workspaces of every display. Enter shows the + workspace. Other actions move the focused window or column there. +- **Run Command** runs a Defi command without arguments: focus, moves, widths, + stacks, the Overview, the cheatsheet, and Settings. See + [Commands](../../CONFIGURATION.md#commands). + +## Finding Defi + +The extension looks for `defi` in `/Applications/Defi.app`, then +`~/Applications/Defi.app`. If Defi lives elsewhere, set **Defi CLI Path** in the +extension's preferences. + +When Defi is not running or its JSON format is newer than the extension, the +lists stay empty and show the reason. diff --git a/contrib/raycast/assets/icon.png b/contrib/raycast/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0b746a67fd22349b525531025593716dfd0b1bb9 GIT binary patch literal 131469 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

G_24C=1F`IyQ?`KPxTl=#cAcY9`px-H_`|7A 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 literal 0 HcmV?d00001 diff --git a/contrib/raycast/assets/icon@dark.png b/contrib/raycast/assets/icon@dark.png new file mode 100644 index 0000000000000000000000000000000000000000..4b0bb7dd531bd4d40207f3f3fbc2306e824b51dd GIT binary patch literal 139021 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%0AfmbG5s@hLM6_c=GWh>f6q*@u&Fl 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 =Pk9Knv?h7%27yFCe@HW05 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 dkhI6-3}W-By~6LuvaA3AQL2 literal 0 HcmV?d00001 diff --git a/contrib/raycast/eslint.config.mjs b/contrib/raycast/eslint.config.mjs new file mode 100644 index 00000000..a1770964 --- /dev/null +++ b/contrib/raycast/eslint.config.mjs @@ -0,0 +1,4 @@ +import { defineConfig } from "eslint/config"; +import raycastConfig from "@raycast/eslint-config"; + +export default defineConfig([...raycastConfig]); diff --git a/contrib/raycast/package-lock.json b/contrib/raycast/package-lock.json new file mode 100644 index 00000000..d942ed82 --- /dev/null +++ b/contrib/raycast/package-lock.json @@ -0,0 +1,3592 @@ +{ + "name": "defi", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "defi", + "license": "MIT", + "dependencies": { + "@raycast/api": "^2.11.1", + "@raycast/utils": "^2.3.3" + }, + "devDependencies": { + "@raycast/eslint-config": "^2.2.0", + "@types/node": "26.6.5", + "@types/react": "19.3.0", + "eslint": "^10.0.1", + "prettier": "^3.8.1", + "typescript": "^5.9.3" + } + }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.3.tgz", + "integrity": "sha512-R76UKbvsqI68DJJVTVyqFKqU98CN4+B+acxNfEA2IFarSzP0SjtKf/3+fFiXzTyv5Fj3PxLkGHe2wnVWWMxobQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/nonrecursive-glob-match": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@isaacs/nonrecursive-glob-match/-/nonrecursive-glob-match-1.1.0.tgz", + "integrity": "sha512-GPm/Dbvk5RLmhRnxXIa7nTpK0xtl4E0wj0MslsuNiJ1FIROU+2+9lgUC/dgF7Ey4QCWhfEsgxilMRMCdHPfDfw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@oclif/core": { + "version": "5.1.12", + "resolved": "https://registry.npmjs.org/@oclif/core/-/core-5.1.12.tgz", + "integrity": "sha512-t8W/ljOb9DIsFNXbfLU4UpZ8+C/X0QuvJOuQXQVCthiw5zSdwKnKoq7HmyBUJwwo28302eiN0rRj8vN9k1U1Mw==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.3.2", + "ansis": "^3.17.0", + "clean-stack": "^3.0.1", + "cli-spinners": "^2.9.2", + "debug": "^4.4.3", + "ejs": "^6", + "get-package-type": "^0.1.0", + "indent-string": "^4.0.0", + "lilconfig": "^3.1.3", + "minimatch": "^11.0.1", + "semver": "^7.8.1", + "string-width": "^4.2.3", + "supports-color": "^8", + "tinyglobby": "^0.2.17", + "widest-line": "^3.1.0", + "wordwrap": "^1.0.0", + "wrap-ansi": "^7.0.0", + "wsl-utils": "^0.4.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@oclif/plugin-autocomplete": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@oclif/plugin-autocomplete/-/plugin-autocomplete-4.0.6.tgz", + "integrity": "sha512-TCdtU3jMG2OJPTIUu9a4qpG/CW6t0ymbk4/GdLfc/o2455AhdYJphIaIpNe772xFhcqceVDg1kWUB5lacDaw2w==", + "license": "MIT", + "dependencies": { + "@oclif/core": "^5.1.9", + "ansis": "^3.16.0", + "debug": "^4.4.1", + "ejs": "^3.1.10" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@oclif/plugin-autocomplete/node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@oclif/plugin-help": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/@oclif/plugin-help/-/plugin-help-7.0.7.tgz", + "integrity": "sha512-k1mWE1bp3AhO6MIKRpdgbGKiPORNCikYRhpGCM7kIz0ywn72/vijoSPOxTfZRbVZyJKFtR25rZ9Lj+QGG2aBNQ==", + "license": "MIT", + "dependencies": { + "@oclif/core": "^5.1.11" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@oclif/plugin-not-found": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@oclif/plugin-not-found/-/plugin-not-found-4.0.7.tgz", + "integrity": "sha512-i1SKG/rnCx/uy2YYzo3mthouiV5sFC+LXbudwRbjDgHEY87uSGPQG02kRYuge5s5XDmn0c8FOqzu6IVbx1aV5w==", + "license": "MIT", + "dependencies": { + "@inquirer/prompts": "^7.10.1", + "@oclif/core": "^5.1.9", + "ansis": "^3.17.0", + "fast-levenshtein": "^3.0.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@raycast/api": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@raycast/api/-/api-2.11.1.tgz", + "integrity": "sha512-O9tCR9QlCa0BhSidkkcNHHii+Y4H8nUO/MRkwpQMXsW0Z/jgSh+GsjG0QeeKS4WHbyhcFPWr7lPbTcr27pRo9A==", + "license": "MIT", + "dependencies": { + "@oclif/core": "^5.1.9", + "@oclif/plugin-autocomplete": "^4.0.5", + "@oclif/plugin-help": "^7.0.5", + "@oclif/plugin-not-found": "^4.0.5", + "@types/node": "26.6.5", + "@types/react": "19.3.0", + "clipboardy": "^5.3.2", + "esbuild": "^0.28.2", + "open": "^11.0.4", + "react": "19.3.1" + }, + "bin": { + "ray": "bin/run.js" + }, + "engines": { + "node": ">=22.22.2" + }, + "peerDependencies": { + "@types/node": "26.6.5", + "@types/react": "19.3.0", + "react-devtools": "8.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "react-devtools": { + "optional": true + } + } + }, + "node_modules/@raycast/eslint-config": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@raycast/eslint-config/-/eslint-config-2.2.0.tgz", + "integrity": "sha512-uS/UDvM+3HfABgYhLYnIv0y+IVu+rGiZZIMwQsO5EGrcf2/TX/yioTzfezAQqhdszW4ViCuHbv8BddIagDKZvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint/js": "^9.39.4", + "@raycast/eslint-plugin": "^2.2.0", + "eslint-config-prettier": "^10.1.8", + "globals": "^17.7.0", + "typescript-eslint": "^8.62.0" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "prettier": ">=2", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@raycast/eslint-plugin": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@raycast/eslint-plugin/-/eslint-plugin-2.2.0.tgz", + "integrity": "sha512-ixO3PoRAEAZZcSQeRPvvDS/vs09D47tXdyXrs7hctoxxPtweXgJ6I24kjCD5GuuuQS5hGTiuL+Uy6hIg6a/a4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.62.0" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/@raycast/utils": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@raycast/utils/-/utils-2.3.3.tgz", + "integrity": "sha512-qtnuoRijFD+NDC4XEgoAyrmuIuqwYtl73RYPwuk/drhRNp0ywktNbb7wI58ZM9vPj+253iF/F5tr0TYwTuz0Fg==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + }, + "peerDependencies": { + "@raycast/api": ">=1.99.4", + "react": ">=19.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "license": "MIT" + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.10.tgz", + "integrity": "sha512-lTyOMnOcmRBzdCv6/4zUZCrEhV0xrqQiUZBZtVcbmLHBXKLnN8q89v/wKRF/Q0HbwVsqbIG3o5HDoFl9sduM+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.6.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", + "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.2.tgz", + "integrity": "sha512-222PKFXKihmFpi/pS5njoBgdAqsPhj2/GBmFbrNw8Tq8gEG9Sv+kSlKFy4u2q+mJv9ZzJgM2Gkv/bUzRZwZw5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.72.2", + "@typescript-eslint/type-utils": "8.72.2", + "@typescript-eslint/utils": "8.72.2", + "@typescript-eslint/visitor-keys": "8.72.2", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.72.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz", + "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.2.tgz", + "integrity": "sha512-llP+T1YuJOLlxEciP6OyQGtz5SMxyn+9LA2BvZw5RmToGVa1y4beEDVQL6Gje7T5WFYh19WREjDCN9Ev4gm5AA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.72.2", + "@typescript-eslint/types": "8.72.2", + "@typescript-eslint/typescript-estree": "8.72.2", + "@typescript-eslint/visitor-keys": "8.72.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.2.tgz", + "integrity": "sha512-6y5dLTZdHhI8Eh3yIQ/bfQhRKwn5e+TpJj64o18Cngc0iW/BCrVvrr6zGYqXPnYH31u6YXAYiFh0+qxX5mybJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.72.2", + "@typescript-eslint/types": "^8.72.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.2.tgz", + "integrity": "sha512-iIoMJ6/8c1/QpFNyFjU0d1YzPco4MsZjQw0k3V+vfzNWAFmUmvhlyMoYEoYYDQZXc+J6Gi1X5XcxlzW2taIIGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.2", + "@typescript-eslint/visitor-keys": "8.72.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.2.tgz", + "integrity": "sha512-+h3iCgZXuTNtoLXcnnjP12ZUHLMqoUcIHPfQrFEXTUqvb9dF8QK9b5nv5VtSo5fLkdHuks8ehbTzZj3rImAm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.2.tgz", + "integrity": "sha512-STfkR38qIdhfkLpgtXnUjAzsNy4xouqiBO0lrVc/Wpk9Kem5/aHXUx1J8dGC16zpdYN1+yc2ILQ28er1kXX7ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.2", + "@typescript-eslint/typescript-estree": "8.72.2", + "@typescript-eslint/utils": "8.72.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.2.tgz", + "integrity": "sha512-Xfn37OLZx03+PCU7qWx9DTv2FCRh1E6fk2+wONeNY15HXIwtcwuveaSwVpGAUptkNmPQegRxW5JyRel9T7wrog==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.2.tgz", + "integrity": "sha512-pQEGqIK/cqEvKivVWaFLYpe7XYejFxfuAOi4mu1oAWFvIo36aRHI9esRFTUxcIy/z8N482RJyH+LpZB0ZIU7Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.72.2", + "@typescript-eslint/tsconfig-utils": "8.72.2", + "@typescript-eslint/types": "8.72.2", + "@typescript-eslint/visitor-keys": "8.72.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.2.tgz", + "integrity": "sha512-hv3eRYbpPbbpezuax1EfGrzpa3C4ySgNZkPUkLOWOAjU8HsOKowEDAr8C7nsNl1C+SBSKDpvno6VKHnlhvp9iA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.72.2", + "@typescript-eslint/types": "8.72.2", + "@typescript-eslint/typescript-estree": "8.72.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.2.tgz", + "integrity": "sha512-NAGrjn0Pitjqzq/GkgMC4kBYm233PQ8GffyQ9+kTA3AlNQYEzOf25EIM02BAHZv5FRJSpHpJk+OGUxVkOB93GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", + "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/bundle-name": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.1.tgz", + "integrity": "sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "license": "MIT" + }, + "node_modules/clean-stack": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-3.0.1.tgz", + "integrity": "sha512-lR9wNiMRcVQjSB3a7xXGLuz4cr4wJuuXlaAEbRutGowQTmlp7R72/DOgN21e8jdwblMWl9UOJMJXarX94pzKdg==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/clipboard-image": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/clipboard-image/-/clipboard-image-0.1.0.tgz", + "integrity": "sha512-SWk7FgaXLNFld19peQ/rTe0n97lwR1WbkqxV6JKCAOh7U52AKV/PeMFCyt/8IhBdqyDA8rdyewQMKZqvWT5Akg==", + "license": "MIT", + "dependencies": { + "run-jxa": "^3.0.0" + }, + "bin": { + "clipboard-image": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-5.3.2.tgz", + "integrity": "sha512-R35PENCHFCw6lsd5SjYPuAVV3Zawr74mKc7ogFNzoDPoQsmWDoJgUNNnWCk/czeqdZGZs8Y0M8zkOlVoySfHEQ==", + "license": "MIT", + "dependencies": { + "clipboard-image": "^0.1.0", + "execa": "^9.6.1", + "is-wayland": "^0.1.0", + "is-wsl": "^3.1.0", + "is64bit": "^2.0.0", + "powershell-utils": "^0.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", + "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", + "license": "MIT", + "dependencies": { + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/crypto-random-string/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/default-browser": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz", + "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ejs": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-6.0.1.tgz", + "integrity": "sha512-UaaM14yby8U3k02ihS1Bmj5Kz2d7CCQM1scxpgs4Mhkq8F1wR2gl3+Ts4h5Ne4Mnt7M9m4Dw7jsuMr3+xO4vZA==", + "license": "Apache-2.0", + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.12.18" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.14.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.14.0.tgz", + "integrity": "sha512-T0pNsxkb9IKmtQjhpK3d7nWOpFv+YHuMbTJnSVMoQblF0onvXfErrtp4V1jHtsQPan7dKaGVD0q1WCXnzMF31A==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.3", + "@humanfs/node": "^0.16.8", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "11.1.5 || >11.1.6 <12", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.6", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", + "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", + "license": "MIT", + "dependencies": { + "fastest-levenshtein": "^1.0.7" + } + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-entry-cache": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^6.1.23" + } + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.14.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.14.0.tgz", + "integrity": "sha512-UtdkTVyAJePQv0J2M9Xe4L0ruNSDV314XiIJsADI2Zf40bcdAonE/D/OhAU8AT99oij9mhLByb+NvA3QWZYgCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-in-ssh": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", + "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wayland": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-wayland/-/is-wayland-0.1.0.tgz", + "integrity": "sha512-QkbMsWkIfkrzOPxenwye0h56iAXirZYHG9eHVPb22fO9y+wPbaX/CHacOWBa/I++4ohTcByimhM1/nyCsH8KNA==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is64bit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is64bit/-/is64bit-2.0.0.tgz", + "integrity": "sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw==", + "license": "MIT", + "dependencies": { + "system-architecture": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.5.tgz", + "integrity": "sha512-aRAOI9Ia43ygL4o1EOmC/sc/0OFxrm7qcQngiIyWaqVnSnz932ybJ07zyCcgMNGSDP8Yxaf88Xj1zHEXmvQ4SA==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/macos-version": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/macos-version/-/macos-version-6.0.0.tgz", + "integrity": "sha512-O2S8voA+pMfCHhBn/TIYDXzJ1qNHpPDU32oFxglKnVdJABiYYITt45oLkV9yhwA3E2FDwn3tQqUFrTsr1p3sBQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "11.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-11.0.8.tgz", + "integrity": "sha512-nFQuYSTVrGvysVBps5Ih+0BdnYFN/Cj3C816r6+p7yK+FHrVLGE4O4N9AFwykrlNlvM2HYt+Rad1Uiro6h88Ig==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/nonrecursive-glob-match": "^1.1.0", + "brace-expansion": "^5.0.12" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/open/-/open-11.0.4.tgz", + "integrity": "sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.5.1", + "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", + "is-inside-container": "^1.0.0", + "powershell-utils": "^0.2.1", + "wsl-utils": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open/node_modules/wsl-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-1.0.1.tgz", + "integrity": "sha512-c16PKLIRxBYpR3jSWsdS7SFp0COm5SOQdiFjY818dYFHJbMGuLBszpF07LBWmOS1uzrHa9VGdynsEgoeKy0oOQ==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0", + "powershell-utils": "^0.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/optionator/node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/powershell-utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.2.1.tgz", + "integrity": "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz", + "integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-ms": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.1.tgz", + "integrity": "sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA==", + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "dev": true, + "license": "MIT" + }, + "node_modules/react": { + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.1.tgz", + "integrity": "sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-jxa": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-jxa/-/run-jxa-3.0.0.tgz", + "integrity": "sha512-4f2CrY7H+sXkKXJn/cE6qRA3z+NMVO7zvlZ/nUV0e62yWftpiLAfw5eV9ZdomzWd2TXWwEIiGjAT57+lWIzzvA==", + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "macos-version": "^6.0.0", + "subsume": "^4.0.0", + "type-fest": "^2.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-jxa/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/run-jxa/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-jxa/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/run-jxa/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-jxa/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/run-jxa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/run-jxa/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/run-jxa/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.6.tgz", + "integrity": "sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/subsume": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/subsume/-/subsume-4.0.0.tgz", + "integrity": "sha512-BWnYJElmHbYZ/zKevy+TG+SsyoFCmRPDHJbR1MzLxkPOv1Jp/4hGhVUtP98s+wZBsBsHwCXvPTP0x287/WMjGg==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^5.0.0", + "unique-string": "^3.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/subsume/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/system-architecture": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/system-architecture/-/system-architecture-0.1.0.tgz", + "integrity": "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.72.2.tgz", + "integrity": "sha512-c/+AKMW1KB81r28Sp0AAdcyXj5dzNmAsZFcsrxKbZTKmcBK2xIwBR31dcSYSDu3bKmzXe8GKrSxO+eDQ401Uzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.72.2", + "@typescript-eslint/parser": "8.72.2", + "@typescript-eslint/typescript-estree": "8.72.2", + "@typescript-eslint/utils": "8.72.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unique-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", + "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", + "license": "MIT", + "dependencies": { + "crypto-random-string": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "license": "MIT", + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wsl-utils": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.4.0.tgz", + "integrity": "sha512-9YmF+2sFEd+T7TkwlmE337F0IVzfDvDknhtpBQxxXzEOfgPphGlFYpyx0cTuCIFj8/p+sqwBYAeGxOMNSzPPDA==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0", + "powershell-utils": "^0.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wsl-utils/node_modules/powershell-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", + "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz", + "integrity": "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/contrib/raycast/package.json b/contrib/raycast/package.json new file mode 100644 index 00000000..ee03d7c9 --- /dev/null +++ b/contrib/raycast/package.json @@ -0,0 +1,65 @@ +{ + "$schema": "https://www.raycast.com/schemas/extension.json", + "name": "defi", + "title": "Defi", + "description": "Search windows and workspaces and run commands of the Defi window manager", + "icon": "icon.png", + "author": "qeude", + "license": "MIT", + "platforms": [ + "macOS" + ], + "categories": [ + "System", + "Productivity" + ], + "commands": [ + { + "name": "search-windows", + "title": "Search Windows", + "description": "Find a managed window and focus it or move it to a workspace", + "mode": "view" + }, + { + "name": "search-workspaces", + "title": "Search Workspaces", + "description": "Show a workspace or move the focused window or column there", + "mode": "view" + }, + { + "name": "run-command", + "title": "Run Command", + "description": "Run a Defi focus, move, width, or Overview command", + "mode": "view" + } + ], + "preferences": [ + { + "name": "defiPath", + "title": "Defi CLI Path", + "description": "Path to the defi binary. Leave empty to use Defi.app in /Applications or ~/Applications.", + "type": "textfield", + "required": false, + "placeholder": "/Applications/Defi.app/Contents/MacOS/defi" + } + ], + "dependencies": { + "@raycast/api": "^2.11.1", + "@raycast/utils": "^2.3.3" + }, + "devDependencies": { + "@raycast/eslint-config": "^2.2.0", + "@types/node": "26.6.5", + "@types/react": "19.3.0", + "eslint": "^10.0.1", + "prettier": "^3.8.1", + "typescript": "^5.9.3" + }, + "scripts": { + "build": "ray build -e dist", + "dev": "ray develop", + "fix-lint": "ray lint --fix", + "lint": "ray lint", + "test": "node --test test/" + } +} diff --git a/contrib/raycast/src/defi.ts b/contrib/raycast/src/defi.ts new file mode 100644 index 00000000..9c7864f8 --- /dev/null +++ b/contrib/raycast/src/defi.ts @@ -0,0 +1,50 @@ +import { closeMainWindow, getPreferenceValues } from "@raycast/api"; +import { showFailureToast } from "@raycast/utils"; +import { execFile } from "node:child_process"; +import { accessSync, constants } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; + +const bundledCLI = "Defi.app/Contents/MacOS/defi"; + +function isExecutable(path: string): boolean { + try { + accessSync(path, constants.X_OK); + return true; + } catch { + return false; + } +} + +function defiPath(): string { + const custom = getPreferenceValues().defiPath?.trim(); + if (custom) { + if (isExecutable(custom)) return custom; + throw new Error(`No executable defi at ${custom}. Fix the path in the extension preferences.`); + } + const found = [join("/Applications", bundledCLI), join(homedir(), "Applications", bundledCLI)].find(isExecutable); + if (!found) + throw new Error("Defi.app was not found in /Applications or ~/Applications. Set its path in the preferences."); + return found; +} + +export function defi(...args: string[]): Promise { + const path = defiPath(); + return new Promise((resolve, reject) => { + execFile(path, args, { timeout: 5000 }, (error, stdout, stderr) => { + if (!error) return resolve(stdout); + const message = stderr.trim() || error.message; + reject(new Error(message.includes("connect failed") ? "Defi is not running. Open Defi and try again." : message)); + }); + }); +} + +// Close Raycast first so the command acts on the window that was focused before it. +export async function runAndClose(...commands: string[][]): Promise { + await closeMainWindow({ clearRootSearch: true }); + try { + for (const args of commands) await defi(...args); + } catch (error) { + await showFailureToast(error, { title: "Defi command failed" }); + } +} diff --git a/contrib/raycast/src/parse.ts b/contrib/raycast/src/parse.ts new file mode 100644 index 00000000..451efcb5 --- /dev/null +++ b/contrib/raycast/src/parse.ts @@ -0,0 +1,68 @@ +export type DefiWindow = { + id: number; + appID?: string; + appName: string; + title: string; + workspace?: { id: string; name?: string; position: number }; + display?: number; + focused: boolean; +}; + +export type DefiWorkspace = { + id: string; + name?: string; + position: number; + kind: string; + display: number; + active: boolean; + windowCount: number; +}; + +// Fields within a version may grow; a new version may break, so it is refused. +function versioned(text: string, version: number, command: string): Record { + const json = JSON.parse(text); + if (json?.version !== version) { + throw new Error( + `defi ${command} returned format version ${json?.version}; this extension reads version ${version}`, + ); + } + return json; +} + +export function parseWindows(text: string): DefiWindow[] { + const { windows } = versioned(text, 1, "list-windows --json"); + if (!Array.isArray(windows)) return []; + return windows + .filter((window) => typeof window?.id === "number") + .map((window) => ({ + id: window.id, + appID: window.appID, + appName: String(window.appName ?? ""), + title: String(window.title ?? ""), + workspace: window.workspace, + display: window.display, + focused: window.focused === true, + })); +} + +export function parseWorkspaces(text: string): DefiWorkspace[] { + const { monitors } = versioned(text, 2, "list-workspaces --json"); + if (!Array.isArray(monitors)) return []; + return monitors.flatMap((monitor) => + (Array.isArray(monitor?.workspaces) ? monitor.workspaces : []) + .filter((workspace: { id?: unknown }) => typeof workspace?.id === "string") + .map((workspace: Record) => ({ + id: workspace.id, + name: workspace.name, + position: workspace.position, + kind: String(workspace.kind ?? ""), + display: monitor.display, + active: workspace.active === true, + windowCount: Number(workspace.windowCount ?? 0), + })), + ); +} + +export function workspaceLabel(workspace: { name?: string; position: number }): string { + return workspace.name ?? `Workspace ${workspace.position}`; +} diff --git a/contrib/raycast/src/run-command.tsx b/contrib/raycast/src/run-command.tsx new file mode 100644 index 00000000..f4773fb2 --- /dev/null +++ b/contrib/raycast/src/run-command.tsx @@ -0,0 +1,114 @@ +import { Action, ActionPanel, Icon, List } from "@raycast/api"; +import { runAndClose } from "./defi"; + +// Argument-free forms of the commands in CONFIGURATION.md, "Commands". +const sections: [string, string[]][] = [ + [ + "Focus", + [ + "focus-column left", + "focus-column right", + "focus-column first", + "focus-column last", + "focus-window up", + "focus-window down", + "focus-window first", + "focus-window last", + "focus-window-previous", + "focus-floating previous", + "focus-floating next", + "activate-floating", + "focus-monitor left", + "focus-monitor right", + "focus-monitor up", + "focus-monitor down", + ], + ], + [ + "Workspaces", + [ + "focus-workspace up", + "focus-workspace down", + "focus-workspace-previous", + "reorder-workspace up", + "reorder-workspace down", + "move-workspace-to-monitor left", + "move-workspace-to-monitor right", + "move-workspace-to-monitor up", + "move-workspace-to-monitor down", + ], + ], + [ + "Move Columns and Windows", + [ + "move-column left", + "move-column right", + "move-column first", + "move-column last", + "move-window up", + "move-window down", + "move-column-to-workspace up", + "move-column-to-workspace down", + "move-window-to-workspace up", + "move-window-to-workspace down", + "move-column-to-monitor left", + "move-column-to-monitor right", + "move-column-to-monitor up", + "move-column-to-monitor down", + "move-window-to-monitor left", + "move-window-to-monitor right", + "move-window-to-monitor up", + "move-window-to-monitor down", + ], + ], + [ + "Widths and Stacks", + [ + "cycle-width previous", + "cycle-width next", + "maximize-column", + "expand-column-to-available-width", + "center-column", + "center-visible-columns", + "switch-preset-window-height", + "reset-window-height", + "join-window left", + "join-window right", + "unjoin-windows", + "consume-or-expel-window left", + "consume-or-expel-window right", + "swap-window left", + "swap-window right", + "toggle-floating", + ], + ], + ["Other", ["toggle-overview", "toggle-cheatsheet", "open-settings"]], +]; + +const title = (command: string) => + command.replace(/(^|[- ])(\w)/g, (_, gap, letter) => (gap && " ") + letter.toUpperCase()); + +export default function Command() { + return ( + + {sections.map(([section, commands]) => ( + + {commands.map((command) => ( + + runAndClose(command.split(" "))} /> + + } + /> + ))} + + ))} + + ); +} diff --git a/contrib/raycast/src/search-windows.tsx b/contrib/raycast/src/search-windows.tsx new file mode 100644 index 00000000..282a87fb --- /dev/null +++ b/contrib/raycast/src/search-windows.tsx @@ -0,0 +1,64 @@ +import { Action, ActionPanel, getApplications, Icon, List } from "@raycast/api"; +import { usePromise } from "@raycast/utils"; +import { defi, runAndClose } from "./defi"; +import { parseWindows, parseWorkspaces, workspaceLabel } from "./parse"; + +export default function Command() { + // list-windows follows layout order; sort by recent use here once feat/recent-windows-switcher exposes it. + const windows = usePromise(async () => parseWindows(await defi("list-windows", "--json"))); + const workspaces = usePromise(async () => parseWorkspaces(await defi("list-workspaces", "--json"))); + const apps = usePromise(getApplications); + const appPaths = new Map(apps.data?.map((app) => [app.bundleId, app.path])); + const displayCount = new Set(workspaces.data?.map((workspace) => workspace.display)).size; + + return ( + + {windows.error ? ( + + ) : ( + + )} + {windows.data?.map((window) => { + const appPath = window.appID ? appPaths.get(window.appID) : undefined; + return ( + 1 ? [{ text: `Display ${window.display}` }] : []), + ]} + actions={ + + runAndClose(["focus-window-id", String(window.id)])} + /> + + {workspaces.data + ?.filter((workspace) => workspace.id !== window.workspace?.id) + .map((workspace) => ( + 1 ? ` (Display ${workspace.display})` : ""}`} + onAction={() => + runAndClose( + ["focus-window-id", String(window.id)], + ["move-window-to-workspace-name", workspace.id], + ) + } + /> + ))} + + + } + /> + ); + })} + + ); +} diff --git a/contrib/raycast/src/search-workspaces.tsx b/contrib/raycast/src/search-workspaces.tsx new file mode 100644 index 00000000..9fa6b25c --- /dev/null +++ b/contrib/raycast/src/search-workspaces.tsx @@ -0,0 +1,54 @@ +import { Action, ActionPanel, Icon, List } from "@raycast/api"; +import { usePromise } from "@raycast/utils"; +import { defi, runAndClose } from "./defi"; +import { parseWorkspaces, workspaceLabel } from "./parse"; + +export default function Command() { + const { data, isLoading, error } = usePromise(async () => parseWorkspaces(await defi("list-workspaces", "--json"))); + const displays = [...new Set(data?.map((workspace) => workspace.display))]; + + return ( + + {error && } + {displays.map((display) => ( + 1 ? `Display ${display}` : undefined}> + {data + ?.filter((workspace) => workspace.display === display) + .map((workspace) => ( + + runAndClose(["--monitor", String(display), "workspace", workspace.id])} + /> + runAndClose(["move-window-to-workspace-name", workspace.id])} + /> + runAndClose(["move-column-to-workspace-name", workspace.id])} + /> + + } + /> + ))} + + ))} + + ); +} diff --git a/contrib/raycast/test/fixtures/list-windows.json b/contrib/raycast/test/fixtures/list-windows.json new file mode 100644 index 00000000..8e40dba9 --- /dev/null +++ b/contrib/raycast/test/fixtures/list-windows.json @@ -0,0 +1,209 @@ +{ + "version": 1, + "windows": [ + { + "appID": "com.apple.mail", + "appName": "Mail", + "column": 1, + "display": 1, + "floating": false, + "focused": false, + "id": 112, + "monitorID": 1, + "row": 1, + "title": "Window 1", + "workspace": { + "id": "dev", + "name": "dev", + "position": 1 + } + }, + { + "appID": "com.t3tools.t3code", + "appName": "T3 Code (Nightly)", + "column": 2, + "display": 1, + "floating": false, + "focused": true, + "id": 2613, + "monitorID": 1, + "row": 1, + "title": "Window 2", + "workspace": { + "id": "dev", + "name": "dev", + "position": 1 + } + }, + { + "appID": "com.apple.finder", + "appName": "Finder", + "column": 3, + "display": 1, + "floating": false, + "focused": false, + "id": 46, + "monitorID": 1, + "row": 1, + "title": "Window 3", + "workspace": { + "id": "dev", + "name": "dev", + "position": 1 + } + }, + { + "appID": "com.apple.dt.Devices", + "appName": "Device Hub", + "column": 4, + "display": 1, + "floating": false, + "focused": false, + "id": 45, + "monitorID": 1, + "row": 1, + "title": "Window 4", + "workspace": { + "id": "dev", + "name": "dev", + "position": 1 + } + }, + { + "appID": "com.ardia.browser", + "appName": "Ardia", + "column": 1, + "display": 1, + "floating": false, + "focused": false, + "id": 51, + "monitorID": 1, + "row": 1, + "title": "Window 5", + "workspace": { + "id": "web", + "name": "web", + "position": 2 + } + }, + { + "appID": "com.mitchellh.ghostty", + "appName": "Ghostty", + "column": 1, + "display": 1, + "floating": false, + "focused": false, + "id": 53, + "monitorID": 1, + "row": 1, + "title": "Window 6", + "workspace": { + "id": "dev-secondary", + "name": "dev-secondary", + "position": 3 + } + }, + { + "appID": "com.mitchellh.ghostty", + "appName": "Ghostty", + "column": 2, + "display": 1, + "floating": false, + "focused": false, + "id": 59, + "monitorID": 1, + "row": 1, + "title": "Window 7", + "workspace": { + "id": "dev-secondary", + "name": "dev-secondary", + "position": 3 + } + }, + { + "appID": "com.mitchellh.ghostty", + "appName": "Ghostty", + "column": 3, + "display": 1, + "floating": false, + "focused": false, + "id": 58, + "monitorID": 1, + "row": 1, + "title": "Window 8", + "workspace": { + "id": "dev-secondary", + "name": "dev-secondary", + "position": 3 + } + }, + { + "appID": "com.mitchellh.ghostty", + "appName": "Ghostty", + "column": 4, + "display": 1, + "floating": false, + "focused": false, + "id": 57, + "monitorID": 1, + "row": 1, + "title": "Window 9", + "workspace": { + "id": "dev-secondary", + "name": "dev-secondary", + "position": 3 + } + }, + { + "appID": "com.mitchellh.ghostty", + "appName": "Ghostty", + "column": 5, + "display": 1, + "floating": false, + "focused": false, + "id": 55, + "monitorID": 1, + "row": 1, + "title": "Window 10", + "workspace": { + "id": "dev-secondary", + "name": "dev-secondary", + "position": 3 + } + }, + { + "appID": "com.mitchellh.ghostty", + "appName": "Ghostty", + "column": 6, + "display": 1, + "floating": false, + "focused": false, + "id": 54, + "monitorID": 1, + "row": 1, + "title": "Window 11", + "workspace": { + "id": "dev-secondary", + "name": "dev-secondary", + "position": 3 + } + }, + { + "appID": "com.mitchellh.ghostty", + "appName": "Ghostty", + "column": 7, + "display": 1, + "floating": false, + "focused": false, + "id": 56, + "monitorID": 1, + "row": 1, + "title": "Window 12", + "workspace": { + "id": "dev-secondary", + "name": "dev-secondary", + "position": 3 + } + } + ] +} diff --git a/contrib/raycast/test/fixtures/list-workspaces.json b/contrib/raycast/test/fixtures/list-workspaces.json new file mode 100644 index 00000000..909250f3 --- /dev/null +++ b/contrib/raycast/test/fixtures/list-workspaces.json @@ -0,0 +1,68 @@ +{ + "focusedMonitorID": 1, + "monitors": [ + { + "activeWorkspace": "dev", + "display": 1, + "focused": true, + "id": 1, + "workspaces": [ + { + "active": true, + "applications": ["com.apple.dt.Devices", "com.apple.finder", "com.apple.mail", "com.t3tools.t3code"], + "focusedApplication": "com.t3tools.t3code", + "icon": "curlybraces", + "id": "dev", + "kind": "named", + "name": "dev", + "occupied": true, + "position": 1, + "windowCount": 4 + }, + { + "active": false, + "applications": ["com.ardia.browser"], + "icon": "globe", + "id": "web", + "kind": "named", + "name": "web", + "occupied": true, + "position": 2, + "windowCount": 1 + }, + { + "active": false, + "applications": ["com.mitchellh.ghostty"], + "icon": "hammer", + "id": "dev-secondary", + "kind": "named", + "name": "dev-secondary", + "occupied": true, + "position": 3, + "windowCount": 7 + }, + { + "active": false, + "applications": [], + "icon": "wrench.and.screwdriver", + "id": "tools", + "kind": "named", + "name": "tools", + "occupied": false, + "position": 4, + "windowCount": 0 + }, + { + "active": false, + "applications": [], + "id": "__defi_dynamic_5", + "kind": "trailing", + "occupied": false, + "position": 5, + "windowCount": 0 + } + ] + } + ], + "version": 2 +} diff --git a/contrib/raycast/test/parse.test.ts b/contrib/raycast/test/parse.test.ts new file mode 100644 index 00000000..5f7794e4 --- /dev/null +++ b/contrib/raycast/test/parse.test.ts @@ -0,0 +1,54 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { test } from "node:test"; +import { parseWindows, parseWorkspaces, workspaceLabel } from "../src/parse.ts"; + +const fixture = (name: string) => readFileSync(new URL(`fixtures/${name}`, import.meta.url), "utf8"); + +test("parses list-windows --json in the order received", () => { + const windows = parseWindows(fixture("list-windows.json")); + assert.equal(windows.length, 12); + assert.deepEqual(windows[1], { + id: 2613, + appID: "com.t3tools.t3code", + appName: "T3 Code (Nightly)", + title: "Window 2", + workspace: { id: "dev", name: "dev", position: 1 }, + display: 1, + focused: true, + }); +}); + +test("parses list-workspaces --json across displays", () => { + const workspaces = parseWorkspaces(fixture("list-workspaces.json")); + assert.deepEqual( + workspaces.map((workspace) => [workspace.id, workspace.display, workspace.kind, workspace.active]), + [ + ["dev", 1, "named", true], + ["web", 1, "named", false], + ["dev-secondary", 1, "named", false], + ["tools", 1, "named", false], + ["__defi_dynamic_5", 1, "trailing", false], + ], + ); + assert.equal(workspaceLabel(workspaces[4]), "Workspace 5"); +}); + +test("ignores unknown fields and malformed entries", () => { + const windows = parseWindows( + JSON.stringify({ + version: 1, + extra: true, + windows: [{ id: 7, title: "T", appName: "A", future: 1 }, { title: "no id" }], + }), + ); + assert.deepEqual(windows, [ + { id: 7, appID: undefined, appName: "A", title: "T", workspace: undefined, display: undefined, focused: false }, + ]); +}); + +test("refuses an unknown format version", () => { + assert.throws(() => parseWindows('{"version":2,"windows":[]}'), /version 2/); + assert.throws(() => parseWorkspaces('{"version":3,"monitors":[]}'), /version 3/); + assert.throws(() => parseWorkspaces("not json")); +}); diff --git a/contrib/raycast/tsconfig.json b/contrib/raycast/tsconfig.json new file mode 100644 index 00000000..61fc480a --- /dev/null +++ b/contrib/raycast/tsconfig.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "include": ["src/**/*", "raycast-env.d.ts"], + "compilerOptions": { + "lib": ["es2023"], + "module": "commonjs", + "target": "es2022", + "strict": true, + "isolatedModules": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react-jsx", + "resolveJsonModule": true + } +} From f79e1ffbf9d76ae7f98633e85ef1f5470ad00072 Mon Sep 17 00:00:00 2001 From: Quentin Eude Date: Wed, 11 Nov 2026 12:21:03 +0100 Subject: [PATCH 02/23] fix(contrib): build the Raycast extension into its own dist folder ray build without -o writes into Raycast's extensions directory. Build into dist so Tinycast can add the folder, and document that route. --- contrib/raycast/README.md | 13 +++++++++++++ contrib/raycast/package.json | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/contrib/raycast/README.md b/contrib/raycast/README.md index e046ed5a..f0200396 100644 --- a/contrib/raycast/README.md +++ b/contrib/raycast/README.md @@ -15,6 +15,19 @@ npm install && npm run dev `npm run dev` adds the extension to Raycast in development mode. +### Tinycast + +[Tinycast](https://github.com/abue-ammar/tinycast) runs the built extension: + +```sh +cd contrib/raycast +npm install && npm run build +``` + +In Tinycast, open Settings > Extensions > Install New > Add from folder and +pick `contrib/raycast/dist`. Tinycast copies the folder, so after an update, +build and add it again. + ## Commands - **Search Windows** lists Defi's managed windows with their app, workspace, and diff --git a/contrib/raycast/package.json b/contrib/raycast/package.json index ee03d7c9..4b3d5257 100644 --- a/contrib/raycast/package.json +++ b/contrib/raycast/package.json @@ -56,7 +56,7 @@ "typescript": "^5.9.3" }, "scripts": { - "build": "ray build -e dist", + "build": "ray build -e dist -o dist", "dev": "ray develop", "fix-lint": "ray lint --fix", "lint": "ray lint", From 509cec9064203fa5e35da5f9057ffcbb195c8e45 Mon Sep 17 00:00:00 2001 From: Quentin Eude Date: Wed, 11 Nov 2026 13:11:06 +0100 Subject: [PATCH 03/23] feat(contrib): show the strip and take arguments in the Raycast extension Search Windows and Search Workspaces show where windows sit in the strip. Run Command sorts by use and turns the search text into widths and workspace targets. Moves confirm with a HUD, and Launch Defi is offered when Defi is not running. --- contrib/raycast/README.md | 23 +++--- contrib/raycast/src/defi.ts | 20 ++++- contrib/raycast/src/error-view.tsx | 14 ++++ contrib/raycast/src/parse.ts | 5 ++ contrib/raycast/src/run-command.tsx | 80 ++++++++++++++----- contrib/raycast/src/search-windows.tsx | 92 +++++++++++++++++----- contrib/raycast/src/search-workspaces.tsx | 94 ++++++++++++++--------- contrib/raycast/src/strip.ts | 62 +++++++++++++++ contrib/raycast/src/suggest.ts | 47 ++++++++++++ contrib/raycast/test/parse.test.ts | 14 +++- contrib/raycast/test/view.test.ts | 54 +++++++++++++ 11 files changed, 413 insertions(+), 92 deletions(-) create mode 100644 contrib/raycast/src/error-view.tsx create mode 100644 contrib/raycast/src/strip.ts create mode 100644 contrib/raycast/src/suggest.ts create mode 100644 contrib/raycast/test/view.test.ts diff --git a/contrib/raycast/README.md b/contrib/raycast/README.md index f0200396..49321029 100644 --- a/contrib/raycast/README.md +++ b/contrib/raycast/README.md @@ -30,14 +30,18 @@ build and add it again. ## Commands -- **Search Windows** lists Defi's managed windows with their app, workspace, and - display. Enter focuses the window. **Move to Workspace** sends it to another - workspace and follows it there. -- **Search Workspaces** lists the workspaces of every display. Enter shows the - workspace. Other actions move the focused window or column there. -- **Run Command** runs a Defi command without arguments: focus, moves, widths, - stacks, the Overview, the cheatsheet, and Settings. See - [Commands](../../CONFIGURATION.md#commands). +- **Search Windows** lists Defi's managed windows. Search by title, app, or + workspace. The side pane shows where the window sits in its workspace's strip. + Enter focuses it; **Previous Window**, at the top, goes back to the window + focused before. ⌘M moves the window to another workspace and follows it. +- **Search Workspaces** lists the workspaces of every display, each with its + strip. Enter shows the workspace. Other actions move the focused window or + column there. +- **Run Command** runs a Defi command, most used first. The search text also + works as an argument: `60` sets the column width to 60%, `+10` or `-10` widens + or narrows it, `960` sets it in points, `3` goes to workspace 3, and a + workspace name offers to go there or move the focused column or window there. + See [Commands](../../CONFIGURATION.md#commands). ## Finding Defi @@ -46,4 +50,5 @@ The extension looks for `defi` in `/Applications/Defi.app`, then extension's preferences. When Defi is not running or its JSON format is newer than the extension, the -lists stay empty and show the reason. +lists stay empty and show the reason; when Defi is not running, Enter launches +it. diff --git a/contrib/raycast/src/defi.ts b/contrib/raycast/src/defi.ts index 9c7864f8..ea0284e5 100644 --- a/contrib/raycast/src/defi.ts +++ b/contrib/raycast/src/defi.ts @@ -1,11 +1,12 @@ -import { closeMainWindow, getPreferenceValues } from "@raycast/api"; +import { closeMainWindow, getPreferenceValues, showHUD } from "@raycast/api"; import { showFailureToast } from "@raycast/utils"; import { execFile } from "node:child_process"; import { accessSync, constants } from "node:fs"; import { homedir } from "node:os"; -import { join } from "node:path"; +import { dirname, join } from "node:path"; const bundledCLI = "Defi.app/Contents/MacOS/defi"; +export const notRunning = "Defi is not running."; function isExecutable(path: string): boolean { try { @@ -28,22 +29,33 @@ function defiPath(): string { return found; } +// The app bundle around the CLI, when the CLI sits in one. +export function defiAppPath(): string | undefined { + try { + const app = dirname(dirname(dirname(defiPath()))); + return app.endsWith(".app") ? app : undefined; + } catch { + return undefined; + } +} + export function defi(...args: string[]): Promise { const path = defiPath(); return new Promise((resolve, reject) => { execFile(path, args, { timeout: 5000 }, (error, stdout, stderr) => { if (!error) return resolve(stdout); const message = stderr.trim() || error.message; - reject(new Error(message.includes("connect failed") ? "Defi is not running. Open Defi and try again." : message)); + reject(new Error(message.includes("connect failed") ? notRunning : message)); }); }); } // Close Raycast first so the command acts on the window that was focused before it. -export async function runAndClose(...commands: string[][]): Promise { +export async function runAndClose(commands: string[][], hud?: string): Promise { await closeMainWindow({ clearRootSearch: true }); try { for (const args of commands) await defi(...args); + if (hud) await showHUD(hud); } catch (error) { await showFailureToast(error, { title: "Defi command failed" }); } diff --git a/contrib/raycast/src/error-view.tsx b/contrib/raycast/src/error-view.tsx new file mode 100644 index 00000000..c175f434 --- /dev/null +++ b/contrib/raycast/src/error-view.tsx @@ -0,0 +1,14 @@ +import { Action, ActionPanel, Icon, List } from "@raycast/api"; +import { defiAppPath, notRunning } from "./defi"; + +export function ErrorView({ title, error }: { title: string; error: Error }) { + const app = error.message === notRunning ? defiAppPath() : undefined; + return ( + {}} + /> + ); +} diff --git a/contrib/raycast/src/parse.ts b/contrib/raycast/src/parse.ts index 451efcb5..6c16395d 100644 --- a/contrib/raycast/src/parse.ts +++ b/contrib/raycast/src/parse.ts @@ -5,6 +5,9 @@ export type DefiWindow = { title: string; workspace?: { id: string; name?: string; position: number }; display?: number; + // 1-based place in the strip; absent for floating windows. + column?: number; + row?: number; focused: boolean; }; @@ -41,6 +44,8 @@ export function parseWindows(text: string): DefiWindow[] { title: String(window.title ?? ""), workspace: window.workspace, display: window.display, + column: window.column, + row: window.row, focused: window.focused === true, })); } diff --git a/contrib/raycast/src/run-command.tsx b/contrib/raycast/src/run-command.tsx index f4773fb2..73c08666 100644 --- a/contrib/raycast/src/run-command.tsx +++ b/contrib/raycast/src/run-command.tsx @@ -1,5 +1,9 @@ import { Action, ActionPanel, Icon, List } from "@raycast/api"; -import { runAndClose } from "./defi"; +import { useFrecencySorting, usePromise } from "@raycast/utils"; +import { useState } from "react"; +import { defi, runAndClose } from "./defi"; +import { parseWorkspaces } from "./parse"; +import { suggestions } from "./suggest"; // Argument-free forms of the commands in CONFIGURATION.md, "Commands". const sections: [string, string[]][] = [ @@ -88,27 +92,63 @@ const sections: [string, string[]][] = [ const title = (command: string) => command.replace(/(^|[- ])(\w)/g, (_, gap, letter) => (gap && " ") + letter.toUpperCase()); +const commands = sections.flatMap(([section, list]) => list.map((command) => ({ id: command, section }))); + export default function Command() { + const [searchText, setSearchText] = useState(""); + const workspaces = usePromise(async () => parseWorkspaces(await defi("list-workspaces", "--json"))); + const { data: sorted, visitItem } = useFrecencySorting(commands); + return ( - - {sections.map(([section, commands]) => ( - - {commands.map((command) => ( - - runAndClose(command.split(" "))} /> - - } - /> - ))} - - ))} + + + {suggestions(searchText, workspaces.data ?? []).map((suggestion) => ( + + runAndClose([suggestion.args], suggestion.hud)} + /> + + } + /> + ))} + + + {sorted.map((item) => ( + + { + await visitItem(item); + await runAndClose([item.id.split(" ")]); + }} + /> + + } + /> + ))} + ); } diff --git a/contrib/raycast/src/search-windows.tsx b/contrib/raycast/src/search-windows.tsx index 282a87fb..773477f1 100644 --- a/contrib/raycast/src/search-windows.tsx +++ b/contrib/raycast/src/search-windows.tsx @@ -1,7 +1,9 @@ -import { Action, ActionPanel, getApplications, Icon, List } from "@raycast/api"; +import { Action, ActionPanel, environment, getApplications, Icon, List } from "@raycast/api"; import { usePromise } from "@raycast/utils"; import { defi, runAndClose } from "./defi"; -import { parseWindows, parseWorkspaces, workspaceLabel } from "./parse"; +import { ErrorView } from "./error-view"; +import { parseWindows, parseWorkspaces, workspaceLabel, type DefiWindow } from "./parse"; +import { escapeMarkdown, stripMarkdown } from "./strip"; export default function Command() { // list-windows follows layout order; sort by recent use here once feat/recent-windows-switcher exposes it. @@ -10,14 +12,52 @@ export default function Command() { const apps = usePromise(getApplications); const appPaths = new Map(apps.data?.map((app) => [app.bundleId, app.path])); const displayCount = new Set(workspaces.data?.map((workspace) => workspace.display)).size; + const dark = environment.appearance === "dark"; + const where = (window: DefiWindow) => + [window.workspace && workspaceLabel(window.workspace), displayCount > 1 && `Display ${window.display}`] + .filter(Boolean) + .join(" · "); + + const detail = (window: DefiWindow) => + [ + `## ${escapeMarkdown(window.appName)}`, + window.title && window.title !== window.appName ? escapeMarkdown(window.title) : "", + window.workspace ? stripMarkdown(windows.data ?? [], window.workspace.id, window.id, dark) : "", + where(window), + ] + .filter(Boolean) + .join("\n\n"); return ( - + {windows.error ? ( - + ) : ( )} + {windows.data?.length ? ( + + } + actions={ + + runAndClose([["focus-window-previous"]])} + /> + + } + /> + ) : null} {windows.data?.map((window) => { const appPath = window.appID ? appPaths.get(window.appID) : undefined; return ( @@ -25,34 +65,44 @@ export default function Command() { key={window.id} icon={appPath ? { fileIcon: appPath } : Icon.Window} title={window.title || window.appName} - subtitle={window.title ? window.appName : undefined} - keywords={[window.appName]} + keywords={[window.appName, ...(window.workspace ? [workspaceLabel(window.workspace)] : [])]} accessories={[ + ...(window.focused ? [{ icon: Icon.CircleFilled, tooltip: "Focused window" }] : []), ...(window.workspace ? [{ tag: workspaceLabel(window.workspace) }] : []), - ...(window.display !== undefined && displayCount > 1 ? [{ text: `Display ${window.display}` }] : []), ]} + detail={} actions={ runAndClose(["focus-window-id", String(window.id)])} + onAction={() => runAndClose([["focus-window-id", String(window.id)]])} /> - + {workspaces.data ?.filter((workspace) => workspace.id !== window.workspace?.id) - .map((workspace) => ( - 1 ? ` (Display ${workspace.display})` : ""}`} - onAction={() => - runAndClose( - ["focus-window-id", String(window.id)], - ["move-window-to-workspace-name", workspace.id], - ) - } - /> - ))} + .map((workspace) => { + const label = workspaceLabel(workspace); + return ( + 1 ? `${label} (Display ${workspace.display})` : label} + onAction={() => + runAndClose( + [ + ["focus-window-id", String(window.id)], + ["move-window-to-workspace-name", workspace.id], + ], + `${window.appName} → ${label}`, + ) + } + /> + ); + })} } diff --git a/contrib/raycast/src/search-workspaces.tsx b/contrib/raycast/src/search-workspaces.tsx index 9fa6b25c..3822d917 100644 --- a/contrib/raycast/src/search-workspaces.tsx +++ b/contrib/raycast/src/search-workspaces.tsx @@ -1,52 +1,72 @@ -import { Action, ActionPanel, Icon, List } from "@raycast/api"; +import { Action, ActionPanel, environment, Icon, List } from "@raycast/api"; import { usePromise } from "@raycast/utils"; import { defi, runAndClose } from "./defi"; -import { parseWorkspaces, workspaceLabel } from "./parse"; +import { ErrorView } from "./error-view"; +import { parseWindows, parseWorkspaces, workspaceLabel, type DefiWorkspace } from "./parse"; +import { stripMarkdown } from "./strip"; export default function Command() { const { data, isLoading, error } = usePromise(async () => parseWorkspaces(await defi("list-workspaces", "--json"))); + const windows = usePromise(async () => parseWindows(await defi("list-windows", "--json"))); const displays = [...new Set(data?.map((workspace) => workspace.display))]; + const focusedID = windows.data?.find((window) => window.focused)?.id; + const dark = environment.appearance === "dark"; + + const detail = (workspace: DefiWorkspace) => + [ + `## ${workspaceLabel(workspace)}`, + stripMarkdown(windows.data ?? [], workspace.id, focusedID, dark), + [ + displays.length > 1 && `Display ${workspace.display}`, + workspace.windowCount === 1 ? "1 window" : `${workspace.windowCount} windows`, + ] + .filter(Boolean) + .join(" · "), + ].join("\n\n"); return ( - - {error && } + + {error && } {displays.map((display) => ( 1 ? `Display ${display}` : undefined}> {data ?.filter((workspace) => workspace.display === display) - .map((workspace) => ( - - runAndClose(["--monitor", String(display), "workspace", workspace.id])} - /> - runAndClose(["move-window-to-workspace-name", workspace.id])} - /> - runAndClose(["move-column-to-workspace-name", workspace.id])} - /> - - } - /> - ))} + .map((workspace) => { + const label = workspaceLabel(workspace); + return ( + } + actions={ + + runAndClose([["--monitor", String(display), "workspace", workspace.id]])} + /> + + runAndClose([["move-window-to-workspace-name", workspace.id]], `Window → ${label}`) + } + /> + + runAndClose([["move-column-to-workspace-name", workspace.id]], `Column → ${label}`) + } + /> + + } + /> + ); + })} ))} diff --git a/contrib/raycast/src/strip.ts b/contrib/raycast/src/strip.ts new file mode 100644 index 00000000..0f0fe2e5 --- /dev/null +++ b/contrib/raycast/src/strip.ts @@ -0,0 +1,62 @@ +import type { DefiWindow } from "./parse"; + +const columnWidth = 36; +const height = 56; +const gap = 6; +const palette = { + light: { window: "#D8D8DD", stroke: "#AEAEB2", highlight: "#007AFF" }, + dark: { window: "#3A3A3C", stroke: "#636366", highlight: "#0A84FF" }, +}; + +export const escapeMarkdown = (text: string) => text.replace(/([\\`*_[\]<>#|])/g, "\\$1"); + +// Tiled windows of one workspace, as columns of stacked windows in strip order. +export function stripColumns(windows: DefiWindow[], workspaceID: string): DefiWindow[][] { + const columns: DefiWindow[][] = []; + for (const window of windows) { + if (window.workspace?.id !== workspaceID || !window.column) continue; + (columns[window.column - 1] ??= []).push(window); + } + return Array.from(columns, (column) => (column ?? []).sort((a, b) => (a.row ?? 0) - (b.row ?? 0))); +} + +export function stripSVG(columns: DefiWindow[][], highlightID: number | undefined, dark: boolean): string { + const colors = dark ? palette.dark : palette.light; + const rects = columns.flatMap((column, index) => { + const rowHeight = (height - gap * (column.length - 1)) / Math.max(column.length, 1); + return column.map((window, row) => { + const fill = window.id === highlightID ? colors.highlight : colors.window; + const x = 1 + index * (columnWidth + gap); + const y = 1 + row * (rowHeight + gap); + return ``; + }); + }); + const width = Math.max(columns.length, 1) * (columnWidth + gap) - gap + 2; + return `${rects.join("")}`; +} + +// Markdown for the detail pane: the strip, then one line naming each column. +export function stripMarkdown( + windows: DefiWindow[], + workspaceID: string, + highlightID: number | undefined, + dark: boolean, +): string { + const columns = stripColumns(windows, workspaceID); + const floating = windows.filter((window) => window.workspace?.id === workspaceID && !window.column); + if (columns.length === 0 && floating.length === 0) return "_No windows._"; + const lines: string[] = []; + if (columns.length > 0) { + const svg = Buffer.from(stripSVG(columns, highlightID, dark)).toString("base64"); + lines.push(`![Strip](data:image/svg+xml;base64,${svg})`); + const names = columns.map((column, index) => { + const label = `${index + 1} ${column.map((window) => escapeMarkdown(window.appName)).join(" + ")}`; + return column.some((window) => window.id === highlightID) ? `**${label}**` : label; + }); + lines.push(names.join(" · ")); + } + if (floating.length > 0) { + lines.push(`Floating: ${floating.map((window) => escapeMarkdown(window.appName)).join(", ")}`); + } + return lines.join("\n\n"); +} diff --git a/contrib/raycast/src/suggest.ts b/contrib/raycast/src/suggest.ts new file mode 100644 index 00000000..0a4ed446 --- /dev/null +++ b/contrib/raycast/src/suggest.ts @@ -0,0 +1,47 @@ +import type { DefiWorkspace } from "./parse"; + +export type Suggestion = { title: string; args: string[]; hud?: string }; + +// Commands whose argument is the search text: "60" sets a width, "+10" widens, "web" targets a workspace. +export function suggestions(query: string, workspaces: DefiWorkspace[]): Suggestion[] { + const text = query.trim(); + if (!text) return []; + const result: Suggestion[] = []; + + const width = text.match(/^([+-])?(\d{1,4})\s*(%|pt|px)?$/i); + if (width) { + const [, sign, digits, unit] = width; + const amount = Number(digits); + if (!sign && !unit && amount >= 1 && amount <= 9) { + result.push({ title: `Go to Workspace ${amount}`, args: ["focus-workspace-position", digits] }); + } + const points = unit ? unit !== "%" : amount > 100; + const shown = points ? `${amount} pt` : `${amount}%`; + // Defi clamps a width to at least 5% of the usable width. + if (sign ? amount > 0 : points || amount >= 5) { + const value = `${sign ?? ""}${amount}${points ? "" : "%"}`; + const title = !sign ? `Set Column Width to ${shown}` : `${sign === "+" ? "Widen" : "Narrow"} Column by ${shown}`; + result.push({ title, args: ["set-column-width", value], hud: title }); + } + } + + const lowered = text.toLowerCase(); + for (const workspace of workspaces) { + const label = workspace.name; + if (!label?.toLowerCase().startsWith(lowered)) continue; + result.push( + { title: `Go to ${label}`, args: ["--monitor", String(workspace.display), "workspace", workspace.id] }, + { + title: `Move Column to ${label}`, + args: ["move-column-to-workspace-name", workspace.id], + hud: `Column → ${label}`, + }, + { + title: `Move Window to ${label}`, + args: ["move-window-to-workspace-name", workspace.id], + hud: `Window → ${label}`, + }, + ); + } + return result; +} diff --git a/contrib/raycast/test/parse.test.ts b/contrib/raycast/test/parse.test.ts index 5f7794e4..7c39953b 100644 --- a/contrib/raycast/test/parse.test.ts +++ b/contrib/raycast/test/parse.test.ts @@ -15,6 +15,8 @@ test("parses list-windows --json in the order received", () => { title: "Window 2", workspace: { id: "dev", name: "dev", position: 1 }, display: 1, + column: 2, + row: 1, focused: true, }); }); @@ -43,7 +45,17 @@ test("ignores unknown fields and malformed entries", () => { }), ); assert.deepEqual(windows, [ - { id: 7, appID: undefined, appName: "A", title: "T", workspace: undefined, display: undefined, focused: false }, + { + id: 7, + appID: undefined, + appName: "A", + title: "T", + workspace: undefined, + display: undefined, + column: undefined, + row: undefined, + focused: false, + }, ]); }); diff --git a/contrib/raycast/test/view.test.ts b/contrib/raycast/test/view.test.ts new file mode 100644 index 00000000..9466a2db --- /dev/null +++ b/contrib/raycast/test/view.test.ts @@ -0,0 +1,54 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { test } from "node:test"; +import { parseWindows, parseWorkspaces } from "../src/parse.ts"; +import { stripColumns, stripMarkdown } from "../src/strip.ts"; +import { suggestions } from "../src/suggest.ts"; + +const fixture = (name: string) => readFileSync(new URL(`fixtures/${name}`, import.meta.url), "utf8"); +const windows = parseWindows(fixture("list-windows.json")); +const workspaces = parseWorkspaces(fixture("list-workspaces.json")); + +test("strip columns follow column and row, skipping floating windows", () => { + const stacked = [ + { id: 1, appName: "A", title: "", focused: false, workspace: { id: "w", position: 1 }, column: 2, row: 2 }, + { id: 2, appName: "B", title: "", focused: false, workspace: { id: "w", position: 1 }, column: 2, row: 1 }, + { id: 3, appName: "C", title: "", focused: false, workspace: { id: "w", position: 1 }, column: 1, row: 1 }, + { id: 4, appName: "D", title: "", focused: false, workspace: { id: "w", position: 1 } }, + ]; + assert.deepEqual( + stripColumns(stacked, "w").map((column) => column.map((window) => window.id)), + [[3], [2, 1]], + ); + const markdown = stripMarkdown(stacked, "w", 1, false); + assert.match(markdown, /^!\[Strip\]\(data:image\/svg\+xml;base64,/); + assert.match(markdown, /1 C · \*\*2 B \+ A\*\*/); + assert.match(markdown, /Floating: D/); +}); + +test("the strip of a real workspace highlights the chosen window", () => { + const svg = Buffer.from(stripMarkdown(windows, "dev", 2613, true).match(/base64,([^)]+)/)![1], "base64").toString(); + assert.equal(svg.match(/ { + const titles = (query: string) => suggestions(query, workspaces).map((s) => [s.title, s.args.join(" ")]); + assert.deepEqual(titles("60"), [["Set Column Width to 60%", "set-column-width 60%"]]); + assert.deepEqual(titles("3"), [["Go to Workspace 3", "focus-workspace-position 3"]]); + assert.deepEqual(titles("7"), [ + ["Go to Workspace 7", "focus-workspace-position 7"], + ["Set Column Width to 7%", "set-column-width 7%"], + ]); + assert.deepEqual(titles("+10"), [["Widen Column by 10%", "set-column-width +10%"]]); + assert.deepEqual(titles("-40pt"), [["Narrow Column by 40 pt", "set-column-width -40"]]); + assert.deepEqual(titles("960"), [["Set Column Width to 960 pt", "set-column-width 960"]]); + assert.deepEqual(titles("We"), [ + ["Go to web", "--monitor 1 workspace web"], + ["Move Column to web", "move-column-to-workspace-name web"], + ["Move Window to web", "move-window-to-workspace-name web"], + ]); + assert.deepEqual(titles("zzz"), []); + assert.deepEqual(titles(" "), []); +}); From 44b9c67244fab6873b08a1954645722cb5627761 Mon Sep 17 00:00:00 2001 From: Quentin Eude Date: Wed, 11 Nov 2026 13:16:43 +0100 Subject: [PATCH 04/23] feat(contrib): build the Raycast extension straight into Tinycast Tinycast reads each command's bundle at launch, so building into its extensions folder updates the extension without reinstalling it. --- contrib/raycast/README.md | 10 +++++----- contrib/raycast/package.json | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/contrib/raycast/README.md b/contrib/raycast/README.md index 49321029..b930230c 100644 --- a/contrib/raycast/README.md +++ b/contrib/raycast/README.md @@ -17,16 +17,16 @@ npm install && npm run dev ### Tinycast -[Tinycast](https://github.com/abue-ammar/tinycast) runs the built extension: +[Tinycast](https://github.com/abue-ammar/tinycast) runs the built extension. +Build it straight into Tinycast's extensions folder: ```sh cd contrib/raycast -npm install && npm run build +npm install && npm run tinycast ``` -In Tinycast, open Settings > Extensions > Install New > Add from folder and -pick `contrib/raycast/dist`. Tinycast copies the folder, so after an update, -build and add it again. +Tinycast reads each command's bundle when the command starts, so after an +update, run `npm run tinycast` again; there is nothing to reinstall. ## Commands diff --git a/contrib/raycast/package.json b/contrib/raycast/package.json index 4b3d5257..bee7f3a5 100644 --- a/contrib/raycast/package.json +++ b/contrib/raycast/package.json @@ -60,6 +60,7 @@ "dev": "ray develop", "fix-lint": "ray lint --fix", "lint": "ray lint", + "tinycast": "ray build -e dist -o \"$HOME/Library/Application Support/com.tinycast.app/extensions/defi\"", "test": "node --test test/" } } From 0a1da2685edd9d2514a61a75bb7ca4d0155c25d0 Mon Sep 17 00:00:00 2001 From: Quentin Eude Date: Wed, 11 Nov 2026 13:25:59 +0100 Subject: [PATCH 05/23] feat(contrib): rework the Raycast extension's lists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Group windows by workspace, show each workspace's apps and number, give commands written titles and icons, and keep the strip behind ⌘D so the lists keep their full width. --- contrib/raycast/README.md | 15 +- contrib/raycast/src/desktop.ts | 22 ++ contrib/raycast/src/error-view.tsx | 8 +- contrib/raycast/src/parse.ts | 2 + contrib/raycast/src/run-command.tsx | 242 ++++++++++++---------- contrib/raycast/src/search-windows.tsx | 204 +++++++++--------- contrib/raycast/src/search-workspaces.tsx | 90 +++++--- contrib/raycast/src/strip.ts | 62 ++---- contrib/raycast/src/suggest.ts | 23 +- contrib/raycast/src/workspace-icon.ts | 8 + contrib/raycast/test/view.test.ts | 17 +- 11 files changed, 398 insertions(+), 295 deletions(-) create mode 100644 contrib/raycast/src/desktop.ts create mode 100644 contrib/raycast/src/workspace-icon.ts diff --git a/contrib/raycast/README.md b/contrib/raycast/README.md index b930230c..335cbaaf 100644 --- a/contrib/raycast/README.md +++ b/contrib/raycast/README.md @@ -30,19 +30,20 @@ update, run `npm run tinycast` again; there is nothing to reinstall. ## Commands -- **Search Windows** lists Defi's managed windows. Search by title, app, or - workspace. The side pane shows where the window sits in its workspace's strip. - Enter focuses it; **Previous Window**, at the top, goes back to the window - focused before. ⌘M moves the window to another workspace and follows it. -- **Search Workspaces** lists the workspaces of every display, each with its - strip. Enter shows the workspace. Other actions move the focused window or - column there. +- **Search Windows** lists Defi's managed windows by workspace. Search by + title, app, or workspace. Enter focuses the window; ⌘M moves it to another + workspace and follows it. +- **Search Workspaces** lists the workspaces of every display with the apps on + each. Enter shows the workspace, ⌘↵ moves the focused window there, and ⌥↵ + moves the focused column. - **Run Command** runs a Defi command, most used first. The search text also works as an argument: `60` sets the column width to 60%, `+10` or `-10` widens or narrows it, `960` sets it in points, `3` goes to workspace 3, and a workspace name offers to go there or move the focused column or window there. See [Commands](../../CONFIGURATION.md#commands). +In both searches, ⌘D shows the workspace's strip beside the list. + ## Finding Defi The extension looks for `defi` in `/Applications/Defi.app`, then diff --git a/contrib/raycast/src/desktop.ts b/contrib/raycast/src/desktop.ts new file mode 100644 index 00000000..7e3d4914 --- /dev/null +++ b/contrib/raycast/src/desktop.ts @@ -0,0 +1,22 @@ +import { getApplications } from "@raycast/api"; +import { usePromise } from "@raycast/utils"; +import { defi } from "./defi"; +import { parseWindows, parseWorkspaces } from "./parse"; + +// Windows, workspaces, and app paths, read together so a view renders once with all of them. +export function useDesktop() { + const { data, isLoading, error } = usePromise(async () => { + const [windows, workspaces, apps] = await Promise.all([ + defi("list-windows", "--json").then(parseWindows), + defi("list-workspaces", "--json").then(parseWorkspaces), + getApplications().catch(() => []), + ]); + return { windows, workspaces, appPaths: new Map(apps.map((app) => [app.bundleId, app.path])) }; + }); + const displayCount = new Set(data?.workspaces.map((workspace) => workspace.display)).size; + const appIcon = (bundleID?: string) => { + const path = bundleID ? data?.appPaths.get(bundleID) : undefined; + return path ? { fileIcon: path } : undefined; + }; + return { ...data, isLoading, error, displayCount, appIcon }; +} diff --git a/contrib/raycast/src/error-view.tsx b/contrib/raycast/src/error-view.tsx index c175f434..e472a2af 100644 --- a/contrib/raycast/src/error-view.tsx +++ b/contrib/raycast/src/error-view.tsx @@ -8,7 +8,13 @@ export function ErrorView({ title, error }: { title: string; error: Error }) { icon={Icon.Warning} title={title} description={error.message} - actions={app && {}} + actions={ + app && ( + + + + ) + } /> ); } diff --git a/contrib/raycast/src/parse.ts b/contrib/raycast/src/parse.ts index 6c16395d..d168fcb7 100644 --- a/contrib/raycast/src/parse.ts +++ b/contrib/raycast/src/parse.ts @@ -19,6 +19,7 @@ export type DefiWorkspace = { display: number; active: boolean; windowCount: number; + applications: string[]; }; // Fields within a version may grow; a new version may break, so it is refused. @@ -64,6 +65,7 @@ export function parseWorkspaces(text: string): DefiWorkspace[] { display: monitor.display, active: workspace.active === true, windowCount: Number(workspace.windowCount ?? 0), + applications: Array.isArray(workspace.applications) ? workspace.applications.map(String) : [], })), ); } diff --git a/contrib/raycast/src/run-command.tsx b/contrib/raycast/src/run-command.tsx index 73c08666..050d14e2 100644 --- a/contrib/raycast/src/run-command.tsx +++ b/contrib/raycast/src/run-command.tsx @@ -3,101 +3,115 @@ import { useFrecencySorting, usePromise } from "@raycast/utils"; import { useState } from "react"; import { defi, runAndClose } from "./defi"; import { parseWorkspaces } from "./parse"; -import { suggestions } from "./suggest"; +import { suggestions, type Suggestion } from "./suggest"; + +type Entry = { command: string; title: string; icon: Icon }; // Argument-free forms of the commands in CONFIGURATION.md, "Commands". -const sections: [string, string[]][] = [ +const sections: [string, Entry[]][] = [ [ "Focus", [ - "focus-column left", - "focus-column right", - "focus-column first", - "focus-column last", - "focus-window up", - "focus-window down", - "focus-window first", - "focus-window last", - "focus-window-previous", - "focus-floating previous", - "focus-floating next", - "activate-floating", - "focus-monitor left", - "focus-monitor right", - "focus-monitor up", - "focus-monitor down", + { command: "focus-column left", title: "Focus Left Column", icon: Icon.ArrowLeft }, + { command: "focus-column right", title: "Focus Right Column", icon: Icon.ArrowRight }, + { command: "focus-column first", title: "Focus First Column", icon: Icon.ArrowLeftCircle }, + { command: "focus-column last", title: "Focus Last Column", icon: Icon.ArrowRightCircle }, + { command: "focus-window up", title: "Focus Window Above", icon: Icon.ArrowUp }, + { command: "focus-window down", title: "Focus Window Below", icon: Icon.ArrowDown }, + { command: "focus-window first", title: "Focus Top Window", icon: Icon.ArrowUpCircle }, + { command: "focus-window last", title: "Focus Bottom Window", icon: Icon.ArrowDownCircle }, + { command: "focus-window-previous", title: "Focus Previous Window", icon: Icon.Undo }, + { command: "focus-floating previous", title: "Focus Previous Floating Window", icon: Icon.ChevronLeft }, + { command: "focus-floating next", title: "Focus Next Floating Window", icon: Icon.ChevronRight }, + { command: "activate-floating", title: "Bring Floating Windows Forward", icon: Icon.Layers }, + { command: "focus-monitor left", title: "Focus Left Display", icon: Icon.Monitor }, + { command: "focus-monitor right", title: "Focus Right Display", icon: Icon.Monitor }, + { command: "focus-monitor up", title: "Focus Display Above", icon: Icon.Monitor }, + { command: "focus-monitor down", title: "Focus Display Below", icon: Icon.Monitor }, ], ], [ "Workspaces", [ - "focus-workspace up", - "focus-workspace down", - "focus-workspace-previous", - "reorder-workspace up", - "reorder-workspace down", - "move-workspace-to-monitor left", - "move-workspace-to-monitor right", - "move-workspace-to-monitor up", - "move-workspace-to-monitor down", + { command: "focus-workspace up", title: "Focus Workspace Above", icon: Icon.ChevronUp }, + { command: "focus-workspace down", title: "Focus Workspace Below", icon: Icon.ChevronDown }, + { command: "focus-workspace-previous", title: "Focus Previous Workspace", icon: Icon.Undo }, + { command: "reorder-workspace up", title: "Move Workspace Up", icon: Icon.ArrowUp }, + { command: "reorder-workspace down", title: "Move Workspace Down", icon: Icon.ArrowDown }, + { command: "move-workspace-to-monitor left", title: "Move Workspace to Left Display", icon: Icon.Monitor }, + { command: "move-workspace-to-monitor right", title: "Move Workspace to Right Display", icon: Icon.Monitor }, + { command: "move-workspace-to-monitor up", title: "Move Workspace to Display Above", icon: Icon.Monitor }, + { command: "move-workspace-to-monitor down", title: "Move Workspace to Display Below", icon: Icon.Monitor }, + ], + ], + [ + "Move", + [ + { command: "move-column left", title: "Move Column Left", icon: Icon.ArrowLeft }, + { command: "move-column right", title: "Move Column Right", icon: Icon.ArrowRight }, + { command: "move-column first", title: "Move Column to Start", icon: Icon.ArrowLeftCircle }, + { command: "move-column last", title: "Move Column to End", icon: Icon.ArrowRightCircle }, + { command: "move-window up", title: "Move Window Up", icon: Icon.ArrowUp }, + { command: "move-window down", title: "Move Window Down", icon: Icon.ArrowDown }, + { command: "move-column-to-workspace up", title: "Move Column to Workspace Above", icon: Icon.ChevronUp }, + { command: "move-column-to-workspace down", title: "Move Column to Workspace Below", icon: Icon.ChevronDown }, + { command: "move-window-to-workspace up", title: "Move Window to Workspace Above", icon: Icon.ChevronUp }, + { command: "move-window-to-workspace down", title: "Move Window to Workspace Below", icon: Icon.ChevronDown }, + { command: "move-column-to-monitor left", title: "Move Column to Left Display", icon: Icon.Monitor }, + { command: "move-column-to-monitor right", title: "Move Column to Right Display", icon: Icon.Monitor }, + { command: "move-column-to-monitor up", title: "Move Column to Display Above", icon: Icon.Monitor }, + { command: "move-column-to-monitor down", title: "Move Column to Display Below", icon: Icon.Monitor }, + { command: "move-window-to-monitor left", title: "Move Window to Left Display", icon: Icon.Monitor }, + { command: "move-window-to-monitor right", title: "Move Window to Right Display", icon: Icon.Monitor }, + { command: "move-window-to-monitor up", title: "Move Window to Display Above", icon: Icon.Monitor }, + { command: "move-window-to-monitor down", title: "Move Window to Display Below", icon: Icon.Monitor }, ], ], [ - "Move Columns and Windows", + "Size and Stacks", [ - "move-column left", - "move-column right", - "move-column first", - "move-column last", - "move-window up", - "move-window down", - "move-column-to-workspace up", - "move-column-to-workspace down", - "move-window-to-workspace up", - "move-window-to-workspace down", - "move-column-to-monitor left", - "move-column-to-monitor right", - "move-column-to-monitor up", - "move-column-to-monitor down", - "move-window-to-monitor left", - "move-window-to-monitor right", - "move-window-to-monitor up", - "move-window-to-monitor down", + { command: "cycle-width next", title: "Next Column Width", icon: Icon.ArrowsExpand }, + { command: "cycle-width previous", title: "Previous Column Width", icon: Icon.ArrowsContract }, + { command: "maximize-column", title: "Maximize Column", icon: Icon.Maximize }, + { command: "expand-column-to-available-width", title: "Fill Available Width", icon: Icon.ArrowsExpand }, + { command: "center-column", title: "Center Column", icon: Icon.Center }, + { command: "center-visible-columns", title: "Center Visible Columns", icon: Icon.AlignCentre }, + { command: "switch-preset-window-height", title: "Next Window Height", icon: Icon.ChevronUpDown }, + { command: "reset-window-height", title: "Reset Window Heights", icon: Icon.RotateAntiClockwise }, + { command: "join-window left", title: "Stack Into Left Column", icon: Icon.AppWindowList }, + { command: "join-window right", title: "Stack Into Right Column", icon: Icon.AppWindowList }, + { command: "unjoin-windows", title: "Unstack Window", icon: Icon.AppWindowGrid2x2 }, + { command: "consume-or-expel-window left", title: "Stack or Unstack Left", icon: Icon.AppWindowSidebarLeft }, + { command: "consume-or-expel-window right", title: "Stack or Unstack Right", icon: Icon.AppWindowSidebarRight }, + { command: "swap-window left", title: "Swap With Left Window", icon: Icon.Switch }, + { command: "swap-window right", title: "Swap With Right Window", icon: Icon.Switch }, + { command: "toggle-floating", title: "Toggle Floating", icon: Icon.Window }, ], ], [ - "Widths and Stacks", + "Defi", [ - "cycle-width previous", - "cycle-width next", - "maximize-column", - "expand-column-to-available-width", - "center-column", - "center-visible-columns", - "switch-preset-window-height", - "reset-window-height", - "join-window left", - "join-window right", - "unjoin-windows", - "consume-or-expel-window left", - "consume-or-expel-window right", - "swap-window left", - "swap-window right", - "toggle-floating", + { command: "toggle-overview", title: "Toggle Overview", icon: Icon.AppWindowGrid3x3 }, + { command: "toggle-cheatsheet", title: "Toggle Cheatsheet", icon: Icon.Keyboard }, + { command: "open-settings", title: "Open Settings", icon: Icon.Gear }, ], ], - ["Other", ["toggle-overview", "toggle-cheatsheet", "open-settings"]], ]; -const title = (command: string) => - command.replace(/(^|[- ])(\w)/g, (_, gap, letter) => (gap && " ") + letter.toUpperCase()); +const entries = sections.flatMap(([section, list]) => list.map((entry) => ({ ...entry, id: entry.command, section }))); -const commands = sections.flatMap(([section, list]) => list.map((command) => ({ id: command, section }))); +const suggestionIcons: Record = { + width: Icon.ArrowsExpand, + workspace: Icon.ArrowRight, + column: Icon.AppWindowSidebarLeft, + window: Icon.Window, +}; export default function Command() { const [searchText, setSearchText] = useState(""); const workspaces = usePromise(async () => parseWorkspaces(await defi("list-workspaces", "--json"))); - const { data: sorted, visitItem } = useFrecencySorting(commands); + const { data: sorted, visitItem } = useFrecencySorting(entries); + const suggested = suggestions(searchText, workspaces.data ?? []); return ( - - {suggestions(searchText, workspaces.data ?? []).map((suggestion) => ( - - runAndClose([suggestion.args], suggestion.hud)} - /> - - } - /> - ))} - - - {sorted.map((item) => ( - - { - await visitItem(item); - await runAndClose([item.id.split(" ")]); - }} - /> - - } - /> - ))} - + {suggested.length > 0 ? ( + + {suggested.map((suggestion) => ( + + runAndClose([suggestion.args], suggestion.hud)} + /> + + } + /> + ))} + + ) : null} + {sections.map(([section]) => ( + + {sorted + .filter((entry) => entry.section === section) + .map((entry) => ( + + { + await visitItem(entry); + await runAndClose([entry.command.split(" ")]); + }} + /> + + } + /> + ))} + + ))} ); } diff --git a/contrib/raycast/src/search-windows.tsx b/contrib/raycast/src/search-windows.tsx index 773477f1..c1daf096 100644 --- a/contrib/raycast/src/search-windows.tsx +++ b/contrib/raycast/src/search-windows.tsx @@ -1,112 +1,128 @@ -import { Action, ActionPanel, environment, getApplications, Icon, List } from "@raycast/api"; -import { usePromise } from "@raycast/utils"; -import { defi, runAndClose } from "./defi"; +import { Action, ActionPanel, environment, Icon, List } from "@raycast/api"; +import { useCachedState } from "@raycast/utils"; +import { runAndClose } from "./defi"; +import { useDesktop } from "./desktop"; import { ErrorView } from "./error-view"; -import { parseWindows, parseWorkspaces, workspaceLabel, type DefiWindow } from "./parse"; -import { escapeMarkdown, stripMarkdown } from "./strip"; +import { workspaceLabel, type DefiWindow } from "./parse"; +import { stripColumns, stripImage } from "./strip"; +import { workspaceIcon } from "./workspace-icon"; export default function Command() { - // list-windows follows layout order; sort by recent use here once feat/recent-windows-switcher exposes it. - const windows = usePromise(async () => parseWindows(await defi("list-windows", "--json"))); - const workspaces = usePromise(async () => parseWorkspaces(await defi("list-workspaces", "--json"))); - const apps = usePromise(getApplications); - const appPaths = new Map(apps.data?.map((app) => [app.bundleId, app.path])); - const displayCount = new Set(workspaces.data?.map((workspace) => workspace.display)).size; + const { windows, workspaces, isLoading, error, displayCount, appIcon } = useDesktop(); + const [showStrip, setShowStrip] = useCachedState("show-strip", false); const dark = environment.appearance === "dark"; - const where = (window: DefiWindow) => - [window.workspace && workspaceLabel(window.workspace), displayCount > 1 && `Display ${window.display}`] - .filter(Boolean) - .join(" · "); - const detail = (window: DefiWindow) => - [ - `## ${escapeMarkdown(window.appName)}`, - window.title && window.title !== window.appName ? escapeMarkdown(window.title) : "", - window.workspace ? stripMarkdown(windows.data ?? [], window.workspace.id, window.id, dark) : "", - where(window), - ] - .filter(Boolean) - .join("\n\n"); + // list-windows follows layout order; sort by recent use here once feat/recent-windows-switcher exposes it. + const sections = new Map(); + for (const window of windows ?? []) { + const key = window.workspace?.id ?? ""; + sections.set(key, [...(sections.get(key) ?? []), window]); + } + + const position = (window: DefiWindow) => { + if (!window.workspace || !window.column) return "Floating"; + const columns = stripColumns(windows ?? [], window.workspace.id); + const stack = columns[window.column - 1]?.length ?? 1; + const place = `Column ${window.column} of ${columns.length}`; + return stack > 1 ? `${place}, window ${window.row} of ${stack}` : place; + }; return ( - {windows.error ? ( - + {error ? ( + ) : ( )} - {windows.data?.length ? ( - - } - actions={ - - runAndClose([["focus-window-previous"]])} - /> - - } - /> - ) : null} - {windows.data?.map((window) => { - const appPath = window.appID ? appPaths.get(window.appID) : undefined; + {[...sections].map(([key, sectionWindows]) => { + const workspace = sectionWindows[0].workspace; + const display = sectionWindows[0].display; return ( - } - actions={ - - runAndClose([["focus-window-id", String(window.id)]])} - /> - - {workspaces.data - ?.filter((workspace) => workspace.id !== window.workspace?.id) - .map((workspace) => { - const label = workspaceLabel(workspace); - return ( - 1 ? `${label} (Display ${workspace.display})` : label} - onAction={() => - runAndClose( - [ - ["focus-window-id", String(window.id)], - ["move-window-to-workspace-name", workspace.id], - ], - `${window.appName} → ${label}`, - ) - } + 1 ? `Display ${display}` : undefined} + > + {sectionWindows.map((window) => ( + + - ); - })} - - - } - /> + {window.title ? : null} + + {workspace ? ( + + ) : null} + {displayCount > 1 ? ( + + ) : null} + + + } + /> + } + actions={ + + runAndClose([["focus-window-id", String(window.id)]])} + /> + + {workspaces + ?.filter((target) => target.id !== window.workspace?.id) + .map((target) => { + const label = workspaceLabel(target); + return ( + 1 ? `${label} on Display ${target.display}` : label} + onAction={() => + runAndClose( + [ + ["focus-window-id", String(window.id)], + ["move-window-to-workspace-name", target.id], + ], + `${window.appName} → ${label}`, + ) + } + /> + ); + })} + + setShowStrip(!showStrip)} + /> + + } + /> + ))} + ); })} diff --git a/contrib/raycast/src/search-workspaces.tsx b/contrib/raycast/src/search-workspaces.tsx index 3822d917..c6a9dfa5 100644 --- a/contrib/raycast/src/search-workspaces.tsx +++ b/contrib/raycast/src/search-workspaces.tsx @@ -1,46 +1,74 @@ import { Action, ActionPanel, environment, Icon, List } from "@raycast/api"; -import { usePromise } from "@raycast/utils"; -import { defi, runAndClose } from "./defi"; +import { useCachedState } from "@raycast/utils"; +import { runAndClose } from "./defi"; +import { useDesktop } from "./desktop"; import { ErrorView } from "./error-view"; -import { parseWindows, parseWorkspaces, workspaceLabel, type DefiWorkspace } from "./parse"; -import { stripMarkdown } from "./strip"; +import { workspaceLabel } from "./parse"; +import { stripImage } from "./strip"; +import { workspaceIcon } from "./workspace-icon"; + +const maxAppIcons = 5; export default function Command() { - const { data, isLoading, error } = usePromise(async () => parseWorkspaces(await defi("list-workspaces", "--json"))); - const windows = usePromise(async () => parseWindows(await defi("list-windows", "--json"))); - const displays = [...new Set(data?.map((workspace) => workspace.display))]; - const focusedID = windows.data?.find((window) => window.focused)?.id; + const { windows, workspaces, isLoading, error, displayCount, appIcon } = useDesktop(); + const [showStrip, setShowStrip] = useCachedState("show-strip", false); const dark = environment.appearance === "dark"; - - const detail = (workspace: DefiWorkspace) => - [ - `## ${workspaceLabel(workspace)}`, - stripMarkdown(windows.data ?? [], workspace.id, focusedID, dark), - [ - displays.length > 1 && `Display ${workspace.display}`, - workspace.windowCount === 1 ? "1 window" : `${workspace.windowCount} windows`, - ] - .filter(Boolean) - .join(" · "), - ].join("\n\n"); + const displays = [...new Set(workspaces?.map((workspace) => workspace.display))]; + const focusedID = windows?.find((window) => window.focused)?.id; + const appName = (bundleID: string) => windows?.find((window) => window.appID === bundleID)?.appName ?? bundleID; return ( - + {error && } {displays.map((display) => ( - 1 ? `Display ${display}` : undefined}> - {data + 1 ? `Display ${display}` : undefined}> + {workspaces ?.filter((workspace) => workspace.display === display) .map((workspace) => { const label = workspaceLabel(workspace); + const icons = workspace.applications.flatMap((app) => { + const icon = appIcon(app); + return icon ? [{ icon, tooltip: appName(app) }] : []; + }); + const extra = icons.length - maxAppIcons; return ( } + subtitle={workspace.windowCount === 0 ? "Empty" : undefined} + keywords={[String(workspace.position), ...workspace.applications]} + accessories={[ + ...icons.slice(0, maxAppIcons), + ...(extra > 0 ? [{ text: `+${extra}` }] : []), + ...(workspace.active ? [{ text: "Active" }] : []), + ]} + detail={ + + + {displayCount > 1 ? ( + + ) : null} + {workspace.applications.length > 0 ? ( + + {workspace.applications.map((app) => { + return ( + + ); + })} + + ) : null} + + } + /> + } actions={ runAndClose([["move-window-to-workspace-name", workspace.id]], `Window → ${label}`) } @@ -58,10 +87,17 @@ export default function Command() { runAndClose([["move-column-to-workspace-name", workspace.id]], `Column → ${label}`) } /> + setShowStrip(!showStrip)} + /> } /> diff --git a/contrib/raycast/src/strip.ts b/contrib/raycast/src/strip.ts index 0f0fe2e5..153216e5 100644 --- a/contrib/raycast/src/strip.ts +++ b/contrib/raycast/src/strip.ts @@ -1,15 +1,11 @@ import type { DefiWindow } from "./parse"; -const columnWidth = 36; -const height = 56; -const gap = 6; +const column = { width: 44, height: 64, gap: 6, inset: 8 }; const palette = { - light: { window: "#D8D8DD", stroke: "#AEAEB2", highlight: "#007AFF" }, - dark: { window: "#3A3A3C", stroke: "#636366", highlight: "#0A84FF" }, + light: { desk: "#00000010", window: "#FFFFFF", stroke: "#0000001F", highlight: "#007AFF" }, + dark: { desk: "#FFFFFF12", window: "#FFFFFF24", stroke: "#FFFFFF1F", highlight: "#0A84FF" }, }; -export const escapeMarkdown = (text: string) => text.replace(/([\\`*_[\]<>#|])/g, "\\$1"); - // Tiled windows of one workspace, as columns of stacked windows in strip order. export function stripColumns(windows: DefiWindow[], workspaceID: string): DefiWindow[][] { const columns: DefiWindow[][] = []; @@ -17,46 +13,32 @@ export function stripColumns(windows: DefiWindow[], workspaceID: string): DefiWi if (window.workspace?.id !== workspaceID || !window.column) continue; (columns[window.column - 1] ??= []).push(window); } - return Array.from(columns, (column) => (column ?? []).sort((a, b) => (a.row ?? 0) - (b.row ?? 0))); + return Array.from(columns, (windows) => (windows ?? []).sort((a, b) => (a.row ?? 0) - (b.row ?? 0))); } +// The strip as an SVG: one rounded tile per window, the highlighted one in the accent color. export function stripSVG(columns: DefiWindow[][], highlightID: number | undefined, dark: boolean): string { const colors = dark ? palette.dark : palette.light; - const rects = columns.flatMap((column, index) => { - const rowHeight = (height - gap * (column.length - 1)) / Math.max(column.length, 1); - return column.map((window, row) => { - const fill = window.id === highlightID ? colors.highlight : colors.window; - const x = 1 + index * (columnWidth + gap); - const y = 1 + row * (rowHeight + gap); - return ``; + const tiles = columns.flatMap((windows, index) => { + const rowHeight = (column.height - column.gap * (windows.length - 1)) / Math.max(windows.length, 1); + return windows.map((window, row) => { + const highlighted = window.id === highlightID; + const x = column.inset + index * (column.width + column.gap); + const y = column.inset + row * (rowHeight + column.gap); + const fill = highlighted ? colors.highlight : colors.window; + return ``; }); }); - const width = Math.max(columns.length, 1) * (columnWidth + gap) - gap + 2; - return `${rects.join("")}`; + const width = column.inset * 2 + Math.max(columns.length, 1) * (column.width + column.gap) - column.gap; + const height = column.inset * 2 + column.height; + const desk = ``; + return `${desk}${tiles.join("")}`; } -// Markdown for the detail pane: the strip, then one line naming each column. -export function stripMarkdown( - windows: DefiWindow[], - workspaceID: string, - highlightID: number | undefined, - dark: boolean, -): string { +// Markdown image of a workspace's strip, or nothing when it has no tiled window. +export function stripImage(windows: DefiWindow[], workspaceID: string, highlightID: number | undefined, dark: boolean) { const columns = stripColumns(windows, workspaceID); - const floating = windows.filter((window) => window.workspace?.id === workspaceID && !window.column); - if (columns.length === 0 && floating.length === 0) return "_No windows._"; - const lines: string[] = []; - if (columns.length > 0) { - const svg = Buffer.from(stripSVG(columns, highlightID, dark)).toString("base64"); - lines.push(`![Strip](data:image/svg+xml;base64,${svg})`); - const names = columns.map((column, index) => { - const label = `${index + 1} ${column.map((window) => escapeMarkdown(window.appName)).join(" + ")}`; - return column.some((window) => window.id === highlightID) ? `**${label}**` : label; - }); - lines.push(names.join(" · ")); - } - if (floating.length > 0) { - lines.push(`Floating: ${floating.map((window) => escapeMarkdown(window.appName)).join(", ")}`); - } - return lines.join("\n\n"); + if (columns.length === 0) return ""; + const svg = Buffer.from(stripSVG(columns, highlightID, dark)).toString("base64"); + return `![Strip](data:image/svg+xml;base64,${svg})`; } diff --git a/contrib/raycast/src/suggest.ts b/contrib/raycast/src/suggest.ts index 0a4ed446..a7582517 100644 --- a/contrib/raycast/src/suggest.ts +++ b/contrib/raycast/src/suggest.ts @@ -1,6 +1,11 @@ import type { DefiWorkspace } from "./parse"; -export type Suggestion = { title: string; args: string[]; hud?: string }; +export type Suggestion = { + title: string; + args: string[]; + kind: "width" | "workspace" | "column" | "window"; + hud?: string; +}; // Commands whose argument is the search text: "60" sets a width, "+10" widens, "web" targets a workspace. export function suggestions(query: string, workspaces: DefiWorkspace[]): Suggestion[] { @@ -13,7 +18,11 @@ export function suggestions(query: string, workspaces: DefiWorkspace[]): Suggest const [, sign, digits, unit] = width; const amount = Number(digits); if (!sign && !unit && amount >= 1 && amount <= 9) { - result.push({ title: `Go to Workspace ${amount}`, args: ["focus-workspace-position", digits] }); + result.push({ + title: `Go to Workspace ${amount}`, + args: ["focus-workspace-position", digits], + kind: "workspace", + }); } const points = unit ? unit !== "%" : amount > 100; const shown = points ? `${amount} pt` : `${amount}%`; @@ -21,7 +30,7 @@ export function suggestions(query: string, workspaces: DefiWorkspace[]): Suggest if (sign ? amount > 0 : points || amount >= 5) { const value = `${sign ?? ""}${amount}${points ? "" : "%"}`; const title = !sign ? `Set Column Width to ${shown}` : `${sign === "+" ? "Widen" : "Narrow"} Column by ${shown}`; - result.push({ title, args: ["set-column-width", value], hud: title }); + result.push({ title, args: ["set-column-width", value], kind: "width", hud: title }); } } @@ -30,15 +39,21 @@ export function suggestions(query: string, workspaces: DefiWorkspace[]): Suggest const label = workspace.name; if (!label?.toLowerCase().startsWith(lowered)) continue; result.push( - { title: `Go to ${label}`, args: ["--monitor", String(workspace.display), "workspace", workspace.id] }, + { + title: `Go to ${label}`, + args: ["--monitor", String(workspace.display), "workspace", workspace.id], + kind: "workspace", + }, { title: `Move Column to ${label}`, args: ["move-column-to-workspace-name", workspace.id], + kind: "column", hud: `Column → ${label}`, }, { title: `Move Window to ${label}`, args: ["move-window-to-workspace-name", workspace.id], + kind: "window", hud: `Window → ${label}`, }, ); diff --git a/contrib/raycast/src/workspace-icon.ts b/contrib/raycast/src/workspace-icon.ts new file mode 100644 index 00000000..1b87b7d8 --- /dev/null +++ b/contrib/raycast/src/workspace-icon.ts @@ -0,0 +1,8 @@ +import { Color, Icon, type Image } from "@raycast/api"; + +// The workspace's position as a numbered icon, tinted when it is the active one. +export function workspaceIcon(workspace: { position: number }, active = false): Image.ImageLike { + const name = `Number${String(workspace.position).padStart(2, "0")}` as keyof typeof Icon; + const source = Icon[name] ?? Icon.AppWindowGrid3x3; + return active ? { source, tintColor: Color.Blue } : source; +} diff --git a/contrib/raycast/test/view.test.ts b/contrib/raycast/test/view.test.ts index 9466a2db..e397699e 100644 --- a/contrib/raycast/test/view.test.ts +++ b/contrib/raycast/test/view.test.ts @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { test } from "node:test"; import { parseWindows, parseWorkspaces } from "../src/parse.ts"; -import { stripColumns, stripMarkdown } from "../src/strip.ts"; +import { stripColumns, stripImage } from "../src/strip.ts"; import { suggestions } from "../src/suggest.ts"; const fixture = (name: string) => readFileSync(new URL(`fixtures/${name}`, import.meta.url), "utf8"); @@ -20,17 +20,14 @@ test("strip columns follow column and row, skipping floating windows", () => { stripColumns(stacked, "w").map((column) => column.map((window) => window.id)), [[3], [2, 1]], ); - const markdown = stripMarkdown(stacked, "w", 1, false); - assert.match(markdown, /^!\[Strip\]\(data:image\/svg\+xml;base64,/); - assert.match(markdown, /1 C · \*\*2 B \+ A\*\*/); - assert.match(markdown, /Floating: D/); }); -test("the strip of a real workspace highlights the chosen window", () => { - const svg = Buffer.from(stripMarkdown(windows, "dev", 2613, true).match(/base64,([^)]+)/)![1], "base64").toString(); - assert.equal(svg.match(/ { + const image = stripImage(windows, "dev", 2613, true); + const svg = Buffer.from(image.match(/^!\[Strip\]\(data:image\/svg\+xml;base64,([^)]+)\)$/)![1], "base64").toString(); + assert.equal(svg.match(/ { From 874ca7cf63c2473f8c0729dad5472e551aa26dda Mon Sep 17 00:00:00 2001 From: Quentin Eude Date: Wed, 11 Nov 2026 14:35:23 +0100 Subject: [PATCH 06/23] feat(contrib): bring the Raycast extension to Store quality Recover from every read failure in place, keep the last state while reloading, name the window each action moves, tag focused and floating windows, add a recent section, raw command subtitles and copy actions to Run Command, take its search as a launch argument, and pick Defi.app in the preferences. --- contrib/raycast/CHANGELOG.md | 7 + contrib/raycast/README.md | 36 ++-- contrib/raycast/package.json | 55 +++-- contrib/raycast/src/defi.ts | 43 ++-- contrib/raycast/src/desktop.ts | 46 ++-- contrib/raycast/src/error-view.tsx | 47 ++-- contrib/raycast/src/parse.ts | 8 +- contrib/raycast/src/run-command.tsx | 103 ++++++--- contrib/raycast/src/search-windows.tsx | 247 +++++++++++++--------- contrib/raycast/src/search-workspaces.tsx | 176 ++++++++------- contrib/raycast/src/strip-detail.tsx | 45 ++++ contrib/raycast/src/strip.ts | 7 +- contrib/raycast/src/suggest.ts | 64 +++--- contrib/raycast/test/parse.test.ts | 5 +- contrib/raycast/test/view.test.ts | 8 +- contrib/raycast/tsconfig.json | 4 +- 16 files changed, 576 insertions(+), 325 deletions(-) create mode 100644 contrib/raycast/CHANGELOG.md create mode 100644 contrib/raycast/src/strip-detail.tsx diff --git a/contrib/raycast/CHANGELOG.md b/contrib/raycast/CHANGELOG.md new file mode 100644 index 00000000..5abc5fa2 --- /dev/null +++ b/contrib/raycast/CHANGELOG.md @@ -0,0 +1,7 @@ +# Defi Changelog + +## [Initial Version] - {PR_MERGE_DATE} + +- Search Windows: focus any managed window, bring it to the current workspace, move it, float it, or copy its title. +- Search Workspaces: jump to a workspace on any display, or move the focused window or column there. +- Run Command: run any Defi command; the search text sets widths and targets workspaces. diff --git a/contrib/raycast/README.md b/contrib/raycast/README.md index 335cbaaf..4f138f6e 100644 --- a/contrib/raycast/README.md +++ b/contrib/raycast/README.md @@ -30,26 +30,28 @@ update, run `npm run tinycast` again; there is nothing to reinstall. ## Commands -- **Search Windows** lists Defi's managed windows by workspace. Search by - title, app, or workspace. Enter focuses the window; ⌘M moves it to another - workspace and follows it. -- **Search Workspaces** lists the workspaces of every display with the apps on - each. Enter shows the workspace, ⌘↵ moves the focused window there, and ⌥↵ - moves the focused column. -- **Run Command** runs a Defi command, most used first. The search text also - works as an argument: `60` sets the column width to 60%, `+10` or `-10` widens - or narrows it, `960` sets it in points, `3` goes to workspace 3, and a - workspace name offers to go there or move the focused column or window there. +- **Search Windows** finds any window Defi manages by title, app, or + workspace. Enter focuses it, ⇧↵ brings it to the current workspace, ⌘M moves + it to another one, and ⌘⇧F floats or tiles it. Windows you pick often come + first; the focused one comes last. +- **Search Workspaces** lists the workspaces of every display with their apps; + search by name, number, or app. Enter shows the workspace, ⌘↵ moves the + focused window there, ⌘⇧↵ sends it without following, and ⌥↵ moves its + column. +- **Run Command** runs any Defi command, with the ones you ran last on top. The + search text also works as an argument: `60` sets the column width to 60%, + `+10` or `-10` widens or narrows it, `960` sets it in points, `2` goes to the + second workspace, and a workspace name offers to go there or move the focused + window or column there. It takes the same text as an argument, so `Run + Command 60` works from the root search. ⌘⇧C copies a command for `[keys]`. See [Commands](../../CONFIGURATION.md#commands). -In both searches, ⌘D shows the workspace's strip beside the list. +In both searches, ⌘Y shows the workspace's strip, and ⌘R refreshes. ## Finding Defi -The extension looks for `defi` in `/Applications/Defi.app`, then -`~/Applications/Defi.app`. If Defi lives elsewhere, set **Defi CLI Path** in the -extension's preferences. +The extension uses Defi.app from `/Applications`, then `~/Applications`. If it +lives elsewhere, pick it in **Defi App** in the extension's preferences. -When Defi is not running or its JSON format is newer than the extension, the -lists stay empty and show the reason; when Defi is not running, Enter launches -it. +When Defi is not running, Enter launches it. The extension makes no network +calls; window titles stay on your Mac. diff --git a/contrib/raycast/package.json b/contrib/raycast/package.json index bee7f3a5..52092886 100644 --- a/contrib/raycast/package.json +++ b/contrib/raycast/package.json @@ -2,7 +2,7 @@ "$schema": "https://www.raycast.com/schemas/extension.json", "name": "defi", "title": "Defi", - "description": "Search windows and workspaces and run commands of the Defi window manager", + "description": "Switch windows, jump between workspaces, and run Defi window manager commands", "icon": "icon.png", "author": "qeude", "license": "MIT", @@ -17,30 +17,59 @@ { "name": "search-windows", "title": "Search Windows", - "description": "Find a managed window and focus it or move it to a workspace", - "mode": "view" + "description": "Find any window Defi manages, then focus it, bring it here, or move it", + "mode": "view", + "keywords": [ + "switch", + "window", + "focus", + "alt tab", + "app" + ] }, { "name": "search-workspaces", "title": "Search Workspaces", - "description": "Show a workspace or move the focused window or column there", - "mode": "view" + "description": "Jump to a workspace on any display, or move the focused window there", + "mode": "view", + "keywords": [ + "space", + "desktop", + "virtual", + "display", + "monitor" + ] }, { "name": "run-command", "title": "Run Command", - "description": "Run a Defi focus, move, width, or Overview command", - "mode": "view" + "description": "Run any Defi command; type 60 for a width or a workspace name to go there", + "mode": "view", + "keywords": [ + "tiling", + "column", + "width", + "layout", + "overview", + "stack" + ], + "arguments": [ + { + "name": "query", + "type": "text", + "placeholder": "60, +10, web", + "required": false + } + ] } ], "preferences": [ { - "name": "defiPath", - "title": "Defi CLI Path", - "description": "Path to the defi binary. Leave empty to use Defi.app in /Applications or ~/Applications.", - "type": "textfield", - "required": false, - "placeholder": "/Applications/Defi.app/Contents/MacOS/defi" + "name": "defiApp", + "title": "Defi App", + "description": "Leave empty to use Defi.app from /Applications or ~/Applications.", + "type": "appPicker", + "required": false } ], "dependencies": { diff --git a/contrib/raycast/src/defi.ts b/contrib/raycast/src/defi.ts index ea0284e5..a202041b 100644 --- a/contrib/raycast/src/defi.ts +++ b/contrib/raycast/src/defi.ts @@ -3,10 +3,10 @@ import { showFailureToast } from "@raycast/utils"; import { execFile } from "node:child_process"; import { accessSync, constants } from "node:fs"; import { homedir } from "node:os"; -import { dirname, join } from "node:path"; +import { join } from "node:path"; -const bundledCLI = "Defi.app/Contents/MacOS/defi"; export const notRunning = "Defi is not running."; +const cliInApp = "Contents/MacOS/defi"; function isExecutable(path: string): boolean { try { @@ -17,32 +17,31 @@ function isExecutable(path: string): boolean { } } -function defiPath(): string { - const custom = getPreferenceValues().defiPath?.trim(); - if (custom) { - if (isExecutable(custom)) return custom; - throw new Error(`No executable defi at ${custom}. Fix the path in the extension preferences.`); - } - const found = [join("/Applications", bundledCLI), join(homedir(), "Applications", bundledCLI)].find(isExecutable); - if (!found) - throw new Error("Defi.app was not found in /Applications or ~/Applications. Set its path in the preferences."); - return found; +// Raycast stores a picked app as an object, Tinycast as its path. +function pickedApp(): string | undefined { + const picked = getPreferenceValues().defiApp as unknown; + const path = typeof picked === "string" ? picked : (picked as { path?: string } | undefined)?.path; + return path?.trim() || undefined; } -// The app bundle around the CLI, when the CLI sits in one. -export function defiAppPath(): string | undefined { - try { - const app = dirname(dirname(dirname(defiPath()))); - return app.endsWith(".app") ? app : undefined; - } catch { - return undefined; +export function defiAppPath(): string { + const picked = pickedApp(); + if (picked) { + if (isExecutable(join(picked, cliInApp))) return picked; + throw new Error(`${picked} has no defi command line tool. Pick Defi.app in the extension preferences.`); } + const found = ["/Applications/Defi.app", join(homedir(), "Applications/Defi.app")].find((app) => + isExecutable(join(app, cliInApp)), + ); + if (!found) + throw new Error("Defi is not installed in Applications. Install it, or pick Defi.app in the preferences."); + return found; } export function defi(...args: string[]): Promise { - const path = defiPath(); + const cli = join(defiAppPath(), cliInApp); return new Promise((resolve, reject) => { - execFile(path, args, { timeout: 5000 }, (error, stdout, stderr) => { + execFile(cli, args, { timeout: 5000 }, (error, stdout, stderr) => { if (!error) return resolve(stdout); const message = stderr.trim() || error.message; reject(new Error(message.includes("connect failed") ? notRunning : message)); @@ -57,6 +56,6 @@ export async function runAndClose(commands: string[][], hud?: string): Promise { - const [windows, workspaces, apps] = await Promise.all([ - defi("list-windows", "--json").then(parseWindows), - defi("list-workspaces", "--json").then(parseWorkspaces), - getApplications().catch(() => []), - ]); - return { windows, workspaces, appPaths: new Map(apps.map((app) => [app.bundleId, app.path])) }; - }); - const displayCount = new Set(data?.workspaces.map((workspace) => workspace.display)).size; - const appIcon = (bundleID?: string) => { - const path = bundleID ? data?.appPaths.get(bundleID) : undefined; - return path ? { fileIcon: path } : undefined; + // The empty view reports the error with its recovery actions, so no toast as well. + const desktop = useCachedPromise(readDesktop, [], { onError: () => undefined }); + const apps = useCachedPromise(async () => + (await getApplications()).map(({ bundleId, name, path }) => ({ bundleId, name, path })), + ); + const appsByID = new Map(apps.data?.map((app) => [app.bundleId, app])); + const windows = desktop.data?.windows; + return { + windows, + workspaces: desktop.data?.workspaces, + isLoading: desktop.isLoading, + error: desktop.error, + revalidate: desktop.revalidate, + displayCount: new Set(desktop.data?.workspaces.map((workspace) => workspace.display)).size, + focused: windows?.find((window) => window.focused), + appIcon: (bundleID?: string) => { + const path = bundleID ? appsByID.get(bundleID)?.path : undefined; + return path ? { fileIcon: path } : undefined; + }, + appName: (bundleID: string) => + appsByID.get(bundleID)?.name ?? windows?.find((window) => window.appID === bundleID)?.appName ?? bundleID, }; - return { ...data, isLoading, error, displayCount, appIcon }; } diff --git a/contrib/raycast/src/error-view.tsx b/contrib/raycast/src/error-view.tsx index e472a2af..f4f0d748 100644 --- a/contrib/raycast/src/error-view.tsx +++ b/contrib/raycast/src/error-view.tsx @@ -1,20 +1,41 @@ -import { Action, ActionPanel, Icon, List } from "@raycast/api"; +import { Action, ActionPanel, Icon, Keyboard, List, openExtensionPreferences } from "@raycast/api"; import { defiAppPath, notRunning } from "./defi"; -export function ErrorView({ title, error }: { title: string; error: Error }) { - const app = error.message === notRunning ? defiAppPath() : undefined; +function launchTarget(): string | undefined { + try { + return defiAppPath(); + } catch { + return undefined; + } +} + +// Recovery for every read failure: launch Defi, try again, or fix the app in the preferences. +export function RecoveryActions({ error, onRetry }: { error: Error; onRetry: () => void }) { + const app = error.message === notRunning ? launchTarget() : undefined; + return ( + + {app ? ( + setTimeout(onRetry, 1500)} /> + ) : null} + + + + ); +} + +export function ErrorView({ error, onRetry }: { error: Error; onRetry: () => void }) { + const stopped = error.message === notRunning; return ( - - - ) - } + icon={stopped ? Icon.Power : Icon.Warning} + title={stopped ? "Defi Is Not Running" : "Cannot Reach Defi"} + description={stopped ? "Press Enter to launch it." : error.message} + actions={} /> ); } diff --git a/contrib/raycast/src/parse.ts b/contrib/raycast/src/parse.ts index d168fcb7..66257c72 100644 --- a/contrib/raycast/src/parse.ts +++ b/contrib/raycast/src/parse.ts @@ -8,6 +8,7 @@ export type DefiWindow = { // 1-based place in the strip; absent for floating windows. column?: number; row?: number; + floating: boolean; focused: boolean; }; @@ -20,6 +21,8 @@ export type DefiWorkspace = { active: boolean; windowCount: number; applications: string[]; + // On the display that holds the focus; position suggestions resolve there. + focusedDisplay: boolean; }; // Fields within a version may grow; a new version may break, so it is refused. @@ -47,6 +50,7 @@ export function parseWindows(text: string): DefiWindow[] { display: window.display, column: window.column, row: window.row, + floating: window.floating === true, focused: window.focused === true, })); } @@ -66,10 +70,12 @@ export function parseWorkspaces(text: string): DefiWorkspace[] { active: workspace.active === true, windowCount: Number(workspace.windowCount ?? 0), applications: Array.isArray(workspace.applications) ? workspace.applications.map(String) : [], + focusedDisplay: monitor.focused === true, })), ); } -export function workspaceLabel(workspace: { name?: string; position: number }): string { +export function workspaceLabel(workspace: { name?: string; position: number; kind?: string }): string { + if (workspace.kind === "trailing") return "New Workspace"; return workspace.name ?? `Workspace ${workspace.position}`; } diff --git a/contrib/raycast/src/run-command.tsx b/contrib/raycast/src/run-command.tsx index 050d14e2..096ebd1c 100644 --- a/contrib/raycast/src/run-command.tsx +++ b/contrib/raycast/src/run-command.tsx @@ -1,7 +1,8 @@ -import { Action, ActionPanel, Icon, List } from "@raycast/api"; -import { useFrecencySorting, usePromise } from "@raycast/utils"; +import { Action, ActionPanel, Icon, Keyboard, LaunchProps, List } from "@raycast/api"; +import { useCachedPromise, useCachedState } from "@raycast/utils"; import { useState } from "react"; -import { defi, runAndClose } from "./defi"; +import { defi, notRunning, runAndClose } from "./defi"; +import { RecoveryActions } from "./error-view"; import { parseWorkspaces } from "./parse"; import { suggestions, type Suggestion } from "./suggest"; @@ -21,8 +22,14 @@ const sections: [string, Entry[]][] = [ { command: "focus-window first", title: "Focus Top Window", icon: Icon.ArrowUpCircle }, { command: "focus-window last", title: "Focus Bottom Window", icon: Icon.ArrowDownCircle }, { command: "focus-window-previous", title: "Focus Previous Window", icon: Icon.Undo }, + { command: "focus-window-or-workspace up", title: "Focus Window or Workspace Above", icon: Icon.ArrowUp }, + { command: "focus-window-or-workspace down", title: "Focus Window or Workspace Below", icon: Icon.ArrowDown }, + { command: "focus-column-or-monitor left", title: "Focus Left Column or Display", icon: Icon.ArrowLeft }, + { command: "focus-column-or-monitor right", title: "Focus Right Column or Display", icon: Icon.ArrowRight }, { command: "focus-floating previous", title: "Focus Previous Floating Window", icon: Icon.ChevronLeft }, { command: "focus-floating next", title: "Focus Next Floating Window", icon: Icon.ChevronRight }, + { command: "focus-floating first", title: "Focus First Floating Window", icon: Icon.ArrowLeftCircle }, + { command: "focus-floating last", title: "Focus Last Floating Window", icon: Icon.ArrowRightCircle }, { command: "activate-floating", title: "Bring Floating Windows Forward", icon: Icon.Layers }, { command: "focus-monitor left", title: "Focus Left Display", icon: Icon.Monitor }, { command: "focus-monitor right", title: "Focus Right Display", icon: Icon.Monitor }, @@ -57,6 +64,12 @@ const sections: [string, Entry[]][] = [ { command: "move-column-to-workspace down", title: "Move Column to Workspace Below", icon: Icon.ChevronDown }, { command: "move-window-to-workspace up", title: "Move Window to Workspace Above", icon: Icon.ChevronUp }, { command: "move-window-to-workspace down", title: "Move Window to Workspace Below", icon: Icon.ChevronDown }, + { command: "send-column-to-workspace up", title: "Send Column to Workspace Above", icon: Icon.ArrowNe }, + { command: "send-column-to-workspace down", title: "Send Column to Workspace Below", icon: Icon.ArrowNe }, + { command: "send-window-to-workspace up", title: "Send Window to Workspace Above", icon: Icon.ArrowNe }, + { command: "send-window-to-workspace down", title: "Send Window to Workspace Below", icon: Icon.ArrowNe }, + { command: "move-column-or-to-monitor left", title: "Move Column Left or to Display", icon: Icon.ArrowLeft }, + { command: "move-column-or-to-monitor right", title: "Move Column Right or to Display", icon: Icon.ArrowRight }, { command: "move-column-to-monitor left", title: "Move Column to Left Display", icon: Icon.Monitor }, { command: "move-column-to-monitor right", title: "Move Column to Right Display", icon: Icon.Monitor }, { command: "move-column-to-monitor up", title: "Move Column to Display Above", icon: Icon.Monitor }, @@ -94,11 +107,13 @@ const sections: [string, Entry[]][] = [ { command: "toggle-overview", title: "Toggle Overview", icon: Icon.AppWindowGrid3x3 }, { command: "toggle-cheatsheet", title: "Toggle Cheatsheet", icon: Icon.Keyboard }, { command: "open-settings", title: "Open Settings", icon: Icon.Gear }, + { command: "diagnostic-mark", title: "Mark a Problem for Diagnostics", icon: Icon.Bug }, ], ], ]; -const entries = sections.flatMap(([section, list]) => list.map((entry) => ({ ...entry, id: entry.command, section }))); +const entries = sections.flatMap(([section, list]) => list.map((entry) => ({ ...entry, section }))); +const recentLimit = 5; const suggestionIcons: Record = { width: Icon.ArrowsExpand, @@ -107,18 +122,60 @@ const suggestionIcons: Record = { window: Icon.Window, }; -export default function Command() { - const [searchText, setSearchText] = useState(""); - const workspaces = usePromise(async () => parseWorkspaces(await defi("list-workspaces", "--json"))); - const { data: sorted, visitItem } = useFrecencySorting(entries); +export default function Command(props: LaunchProps<{ arguments: Arguments.RunCommand }>) { + const [searchText, setSearchText] = useState(props.arguments?.query ?? props.fallbackText ?? ""); + const [recent, setRecent] = useCachedState("recent-commands", []); + const workspaces = useCachedPromise(async () => parseWorkspaces(await defi("list-workspaces", "--json")), [], { + onError: () => undefined, + }); const suggested = suggestions(searchText, workspaces.data ?? []); + const recentEntries = searchText + ? [] + : recent.flatMap((command) => entries.filter((entry) => entry.command === command)); + + const run = (command: string) => { + setRecent([command, ...recent.filter((other) => other !== command)].slice(0, recentLimit)); + return runAndClose([command.split(" ")]); + }; + + const item = (entry: (typeof entries)[number], keyPrefix = "") => ( + + run(entry.command)} /> + + + } + /> + ); return ( + {workspaces.error?.message === notRunning ? ( + + } + /> + + ) : null} {suggested.length > 0 ? ( {suggested.map((suggestion) => ( @@ -141,30 +198,12 @@ export default function Command() { ))} ) : null} + {recentEntries.length > 0 ? ( + {recentEntries.map((entry) => item(entry, "recent-"))} + ) : null} {sections.map(([section]) => ( - {sorted - .filter((entry) => entry.section === section) - .map((entry) => ( - - { - await visitItem(entry); - await runAndClose([entry.command.split(" ")]); - }} - /> - - } - /> - ))} + {entries.filter((entry) => entry.section === section).map((entry) => item(entry))} ))} diff --git a/contrib/raycast/src/search-windows.tsx b/contrib/raycast/src/search-windows.tsx index c1daf096..377b7cf7 100644 --- a/contrib/raycast/src/search-windows.tsx +++ b/contrib/raycast/src/search-windows.tsx @@ -1,128 +1,175 @@ -import { Action, ActionPanel, environment, Icon, List } from "@raycast/api"; -import { useCachedState } from "@raycast/utils"; +import { Action, ActionPanel, Color, environment, Icon, Keyboard, List } from "@raycast/api"; +import { useCachedState, useFrecencySorting } from "@raycast/utils"; import { runAndClose } from "./defi"; import { useDesktop } from "./desktop"; import { ErrorView } from "./error-view"; import { workspaceLabel, type DefiWindow } from "./parse"; -import { stripColumns, stripImage } from "./strip"; +import { StripDetail } from "./strip-detail"; +import { stripColumns } from "./strip"; import { workspaceIcon } from "./workspace-icon"; export default function Command() { - const { windows, workspaces, isLoading, error, displayCount, appIcon } = useDesktop(); + const desktop = useDesktop(); + const { windows, workspaces, focused, displayCount, appIcon } = desktop; const [showStrip, setShowStrip] = useCachedState("show-strip", false); + // shortcut: ranks windows by picks made here, not by Defi's focus history; use that order once + // list-windows exposes it (feat/recent-windows-switcher keeps one). + const { data: ranked, visitItem } = useFrecencySorting(windows, { key: (window) => String(window.id) }); + // The focused window goes last: Enter on it would change nothing. + const sorted = [...ranked.filter((window) => !window.focused), ...ranked.filter((window) => window.focused)]; const dark = environment.appearance === "dark"; - // list-windows follows layout order; sort by recent use here once feat/recent-windows-switcher exposes it. - const sections = new Map(); - for (const window of windows ?? []) { - const key = window.workspace?.id ?? ""; - sections.set(key, [...(sections.get(key) ?? []), window]); - } - const position = (window: DefiWindow) => { if (!window.workspace || !window.column) return "Floating"; const columns = stripColumns(windows ?? [], window.workspace.id); const stack = columns[window.column - 1]?.length ?? 1; const place = `Column ${window.column} of ${columns.length}`; - return stack > 1 ? `${place}, window ${window.row} of ${stack}` : place; + return stack > 1 ? `${place}, row ${window.row} of ${stack}` : place; + }; + + const focus = async (window: DefiWindow, then: string[][] = [], hud?: string) => { + await visitItem(window); + await runAndClose([["focus-window-id", String(window.id)], ...then], hud); }; return ( 0} searchBarPlaceholder="Search windows by title, app, or workspace" > - {error ? ( - - ) : ( - + {desktop.error ? ( + + ) : desktop.isLoading ? null : ( + )} - {[...sections].map(([key, sectionWindows]) => { - const workspace = sectionWindows[0].workspace; - const display = sectionWindows[0].display; + {sorted.map((window) => { + const workspace = window.workspace; + const label = workspace ? workspaceLabel(workspace) : undefined; + const here = focused?.workspace; return ( - 1 ? `Display ${display}` : undefined} - > - {sectionWindows.map((window) => ( - - - {window.title ? : null} - - {workspace ? ( - - ) : null} - {displayCount > 1 ? ( - - ) : null} - - - } - /> - } - actions={ - - runAndClose([["focus-window-id", String(window.id)]])} - /> - - {workspaces - ?.filter((target) => target.id !== window.workspace?.id) - .map((target) => { - const label = workspaceLabel(target); - return ( - 1 ? `${label} on Display ${target.display}` : label} - onAction={() => - runAndClose( - [ - ["focus-window-id", String(window.id)], - ["move-window-to-workspace-name", target.id], - ], - `${window.appName} → ${label}`, - ) - } - /> - ); - })} - + 1 ? `${label} on display ${window.display}` : label, + }, + ] + : []), + ]} + detail={ + workspace ? ( + + + {displayCount > 1 ? ( + + ) : null} + + + + ) : undefined + } + actions={ + + + focus(window)} /> + {here && workspace && here.id !== workspace.id ? ( setShowStrip(!showStrip)} + title={`Bring to ${workspaceLabel(here)}`} + icon={Icon.ArrowDownCircle} + shortcut={{ modifiers: ["shift"], key: "return" }} + onAction={() => + focus( + window, + [["move-window-to-workspace-name", here.id]], + `${window.appName} → ${workspaceLabel(here)}`, + ) + } /> - - } - /> - ))} - + ) : null} + + + + {workspaces + ?.filter((target) => target.id !== workspace?.id) + .map((target) => { + const name = workspaceLabel(target); + return ( + 1 ? `${name} on Display ${target.display}` : name} + onAction={() => + focus( + window, + [["move-window-to-workspace-name", target.id]], + `${window.appName} → ${name}`, + ) + } + /> + ); + })} + + focus(window, [["toggle-floating"]])} + /> + + + + + + + setShowStrip(!showStrip)} + /> + + + + } + /> ); })} diff --git a/contrib/raycast/src/search-workspaces.tsx b/contrib/raycast/src/search-workspaces.tsx index c6a9dfa5..c1d1756e 100644 --- a/contrib/raycast/src/search-workspaces.tsx +++ b/contrib/raycast/src/search-workspaces.tsx @@ -1,30 +1,98 @@ -import { Action, ActionPanel, environment, Icon, List } from "@raycast/api"; +import { Action, ActionPanel, Color, environment, Icon, Keyboard, List } from "@raycast/api"; import { useCachedState } from "@raycast/utils"; import { runAndClose } from "./defi"; import { useDesktop } from "./desktop"; import { ErrorView } from "./error-view"; -import { workspaceLabel } from "./parse"; -import { stripImage } from "./strip"; +import { workspaceLabel, type DefiWorkspace } from "./parse"; +import { StripDetail } from "./strip-detail"; import { workspaceIcon } from "./workspace-icon"; const maxAppIcons = 5; export default function Command() { - const { windows, workspaces, isLoading, error, displayCount, appIcon } = useDesktop(); + const desktop = useDesktop(); + const { windows, workspaces, focused, displayCount, appIcon, appName } = desktop; const [showStrip, setShowStrip] = useCachedState("show-strip", false); const dark = environment.appearance === "dark"; const displays = [...new Set(workspaces?.map((workspace) => workspace.display))]; - const focusedID = windows?.find((window) => window.focused)?.id; - const appName = (bundleID: string) => windows?.find((window) => window.appID === bundleID)?.appName ?? bundleID; + const focusedName = focused?.appName ?? "Window"; + + const actions = (workspace: DefiWorkspace, label: string) => { + const display = String(workspace.display); + const elsewhere = focused && focused.workspace?.id !== workspace.id; + return ( + + + runAndClose([["--monitor", display, "workspace", workspace.id]])} + /> + + {elsewhere ? ( + + + runAndClose([["move-window-to-workspace-name", workspace.id]], `${focusedName} → ${label}`) + } + /> + + runAndClose([["send-window-to-workspace-name", workspace.id]], `${focusedName} → ${label}`) + } + /> + runAndClose([["move-column-to-workspace-name", workspace.id]], `Column → ${label}`)} + /> + + ) : null} + + setShowStrip(!showStrip)} + /> + + + + ); + }; return ( - - {error && } - {displays.map((display) => ( - 1 ? `Display ${display}` : undefined}> - {workspaces - ?.filter((workspace) => workspace.display === display) - .map((workspace) => { + + {desktop.error ? ( + + ) : ( + + )} + {displays.map((display) => { + const onDisplay = workspaces?.filter((workspace) => workspace.display === display) ?? []; + return ( + 1 ? `Display ${display}` : undefined} + subtitle={displayCount > 1 && onDisplay[0]?.focusedDisplay ? "Focused" : undefined} + > + {onDisplay.map((workspace) => { const label = workspaceLabel(workspace); const icons = workspace.applications.flatMap((app) => { const icon = appIcon(app); @@ -34,77 +102,39 @@ export default function Command() { return ( 0 ? [{ text: `+${extra}` }] : []), - ...(workspace.active ? [{ text: "Active" }] : []), + ...(workspace.active ? [{ tag: { value: "Active", color: Color.Blue } }] : []), ]} detail={ - - - {displayCount > 1 ? ( - - ) : null} - {workspace.applications.length > 0 ? ( - - {workspace.applications.map((app) => { - return ( - - ); - })} - - ) : null} - - } - /> - } - actions={ - - runAndClose([["--monitor", String(display), "workspace", workspace.id]])} - /> - - runAndClose([["move-window-to-workspace-name", workspace.id]], `Window → ${label}`) - } - /> - - runAndClose([["move-column-to-workspace-name", workspace.id]], `Column → ${label}`) - } - /> - setShowStrip(!showStrip)} + + - + {displayCount > 1 ? ( + + ) : null} + + } + actions={actions(workspace, label)} /> ); })} - - ))} + + ); + })} ); } diff --git a/contrib/raycast/src/strip-detail.tsx b/contrib/raycast/src/strip-detail.tsx new file mode 100644 index 00000000..5b3f4f92 --- /dev/null +++ b/contrib/raycast/src/strip-detail.tsx @@ -0,0 +1,45 @@ +import { Image, List } from "@raycast/api"; +import type { DefiWindow } from "./parse"; +import { stripColumns, stripImage } from "./strip"; + +type Props = { + windows: DefiWindow[]; + workspaceID: string; + highlightID?: number; + dark: boolean; + appIcon: (bundleID?: string) => Image.ImageLike | undefined; + children?: React.ReactNode; +}; + +// The workspace's strip with its columns named in order, under the rows the caller adds first. +export function StripDetail({ windows, workspaceID, highlightID, dark, appIcon, children }: Props) { + const columns = stripColumns(windows, workspaceID); + const floating = windows.filter((window) => window.workspace?.id === workspaceID && window.floating); + const tags = (list: DefiWindow[]) => + list.map((window) => ( + + )); + return ( + + {children} + {columns.map((column, index) => ( + + {tags(column)} + + ))} + {floating.length > 0 ? ( + {tags(floating)} + ) : null} + + } + /> + ); +} diff --git a/contrib/raycast/src/strip.ts b/contrib/raycast/src/strip.ts index 153216e5..9a186a85 100644 --- a/contrib/raycast/src/strip.ts +++ b/contrib/raycast/src/strip.ts @@ -1,6 +1,7 @@ import type { DefiWindow } from "./parse"; const column = { width: 44, height: 64, gap: 6, inset: 8 }; +const minWidth = 240; const palette = { light: { desk: "#00000010", window: "#FFFFFF", stroke: "#0000001F", highlight: "#007AFF" }, dark: { desk: "#FFFFFF12", window: "#FFFFFF24", stroke: "#FFFFFF1F", highlight: "#0A84FF" }, @@ -19,17 +20,19 @@ export function stripColumns(windows: DefiWindow[], workspaceID: string): DefiWi // The strip as an SVG: one rounded tile per window, the highlighted one in the accent color. export function stripSVG(columns: DefiWindow[][], highlightID: number | undefined, dark: boolean): string { const colors = dark ? palette.dark : palette.light; + const stripWidth = Math.max(columns.length, 1) * (column.width + column.gap) - column.gap; + const width = Math.max(minWidth, column.inset * 2 + stripWidth); + const left = (width - stripWidth) / 2; const tiles = columns.flatMap((windows, index) => { const rowHeight = (column.height - column.gap * (windows.length - 1)) / Math.max(windows.length, 1); return windows.map((window, row) => { const highlighted = window.id === highlightID; - const x = column.inset + index * (column.width + column.gap); + const x = left + index * (column.width + column.gap); const y = column.inset + row * (rowHeight + column.gap); const fill = highlighted ? colors.highlight : colors.window; return ``; }); }); - const width = column.inset * 2 + Math.max(columns.length, 1) * (column.width + column.gap) - column.gap; const height = column.inset * 2 + column.height; const desk = ``; return `${desk}${tiles.join("")}`; diff --git a/contrib/raycast/src/suggest.ts b/contrib/raycast/src/suggest.ts index a7582517..520f849b 100644 --- a/contrib/raycast/src/suggest.ts +++ b/contrib/raycast/src/suggest.ts @@ -1,4 +1,4 @@ -import type { DefiWorkspace } from "./parse"; +import { workspaceLabel, type DefiWorkspace } from "./parse.ts"; export type Suggestion = { title: string; @@ -7,7 +7,36 @@ export type Suggestion = { hud?: string; }; -// Commands whose argument is the search text: "60" sets a width, "+10" widens, "web" targets a workspace. +function workspaceSuggestions(workspace: DefiWorkspace): Suggestion[] { + const label = workspaceLabel(workspace); + return [ + { + title: `Go to ${label}`, + args: ["--monitor", String(workspace.display), "workspace", workspace.id], + kind: "workspace", + }, + { + title: `Move Column to ${label}`, + args: ["move-column-to-workspace-name", workspace.id], + kind: "column", + hud: `Column → ${label}`, + }, + { + title: `Move Window to ${label}`, + args: ["move-window-to-workspace-name", workspace.id], + kind: "window", + hud: `Window → ${label}`, + }, + { + title: `Send Window to ${label}`, + args: ["send-window-to-workspace-name", workspace.id], + kind: "window", + hud: `Window → ${label}`, + }, + ]; +} + +// Commands whose argument is the search text: "60" sets a width, "+10" widens, "web" or "2" targets a workspace. export function suggestions(query: string, workspaces: DefiWorkspace[]): Suggestion[] { const text = query.trim(); if (!text) return []; @@ -17,13 +46,8 @@ export function suggestions(query: string, workspaces: DefiWorkspace[]): Suggest if (width) { const [, sign, digits, unit] = width; const amount = Number(digits); - if (!sign && !unit && amount >= 1 && amount <= 9) { - result.push({ - title: `Go to Workspace ${amount}`, - args: ["focus-workspace-position", digits], - kind: "workspace", - }); - } + const byPosition = !sign && !unit && workspaces.find((w) => w.focusedDisplay && w.position === amount); + if (byPosition) result.push(workspaceSuggestions(byPosition)[0]); const points = unit ? unit !== "%" : amount > 100; const shown = points ? `${amount} pt` : `${amount}%`; // Defi clamps a width to at least 5% of the usable width. @@ -36,27 +60,7 @@ export function suggestions(query: string, workspaces: DefiWorkspace[]): Suggest const lowered = text.toLowerCase(); for (const workspace of workspaces) { - const label = workspace.name; - if (!label?.toLowerCase().startsWith(lowered)) continue; - result.push( - { - title: `Go to ${label}`, - args: ["--monitor", String(workspace.display), "workspace", workspace.id], - kind: "workspace", - }, - { - title: `Move Column to ${label}`, - args: ["move-column-to-workspace-name", workspace.id], - kind: "column", - hud: `Column → ${label}`, - }, - { - title: `Move Window to ${label}`, - args: ["move-window-to-workspace-name", workspace.id], - kind: "window", - hud: `Window → ${label}`, - }, - ); + if (workspace.name?.toLowerCase().startsWith(lowered)) result.push(...workspaceSuggestions(workspace)); } return result; } diff --git a/contrib/raycast/test/parse.test.ts b/contrib/raycast/test/parse.test.ts index 7c39953b..2605d3c6 100644 --- a/contrib/raycast/test/parse.test.ts +++ b/contrib/raycast/test/parse.test.ts @@ -17,6 +17,7 @@ test("parses list-windows --json in the order received", () => { display: 1, column: 2, row: 1, + floating: false, focused: true, }); }); @@ -33,7 +34,8 @@ test("parses list-workspaces --json across displays", () => { ["__defi_dynamic_5", 1, "trailing", false], ], ); - assert.equal(workspaceLabel(workspaces[4]), "Workspace 5"); + assert.equal(workspaceLabel(workspaces[4]), "New Workspace"); + assert.equal(workspaceLabel({ position: 2 }), "Workspace 2"); }); test("ignores unknown fields and malformed entries", () => { @@ -54,6 +56,7 @@ test("ignores unknown fields and malformed entries", () => { display: undefined, column: undefined, row: undefined, + floating: false, focused: false, }, ]); diff --git a/contrib/raycast/test/view.test.ts b/contrib/raycast/test/view.test.ts index e397699e..0a2d64eb 100644 --- a/contrib/raycast/test/view.test.ts +++ b/contrib/raycast/test/view.test.ts @@ -33,11 +33,8 @@ test("the strip image draws one tile per tiled window and highlights one", () => test("search text becomes a command argument", () => { const titles = (query: string) => suggestions(query, workspaces).map((s) => [s.title, s.args.join(" ")]); assert.deepEqual(titles("60"), [["Set Column Width to 60%", "set-column-width 60%"]]); - assert.deepEqual(titles("3"), [["Go to Workspace 3", "focus-workspace-position 3"]]); - assert.deepEqual(titles("7"), [ - ["Go to Workspace 7", "focus-workspace-position 7"], - ["Set Column Width to 7%", "set-column-width 7%"], - ]); + assert.deepEqual(titles("3"), [["Go to dev-secondary", "--monitor 1 workspace dev-secondary"]]); + assert.deepEqual(titles("7"), [["Set Column Width to 7%", "set-column-width 7%"]]); assert.deepEqual(titles("+10"), [["Widen Column by 10%", "set-column-width +10%"]]); assert.deepEqual(titles("-40pt"), [["Narrow Column by 40 pt", "set-column-width -40"]]); assert.deepEqual(titles("960"), [["Set Column Width to 960 pt", "set-column-width 960"]]); @@ -45,6 +42,7 @@ test("search text becomes a command argument", () => { ["Go to web", "--monitor 1 workspace web"], ["Move Column to web", "move-column-to-workspace-name web"], ["Move Window to web", "move-window-to-workspace-name web"], + ["Send Window to web", "send-window-to-workspace-name web"], ]); assert.deepEqual(titles("zzz"), []); assert.deepEqual(titles(" "), []); diff --git a/contrib/raycast/tsconfig.json b/contrib/raycast/tsconfig.json index 61fc480a..c2a1b8eb 100644 --- a/contrib/raycast/tsconfig.json +++ b/contrib/raycast/tsconfig.json @@ -11,6 +11,8 @@ "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "jsx": "react-jsx", - "resolveJsonModule": true + "resolveJsonModule": true, + "allowImportingTsExtensions": true, + "noEmit": true } } From 27087061688ac0c7a5100240935b4d137456592e Mon Sep 17 00:00:00 2001 From: Quentin Eude Date: Wed, 11 Nov 2026 14:43:29 +0100 Subject: [PATCH 07/23] feat(contrib): steady shortcuts, live lists, and command help in Raycast Keep the secondary action the same on every row, follow defi subscribe while a list is open, filter windows by workspace, show configured workspace symbols, explain each command with its default shortcut, and guide a missing Defi install. --- contrib/raycast/README.md | 17 +- contrib/raycast/src/commands.ts | 231 ++++++++++++++++++++++ contrib/raycast/src/defi.ts | 22 ++- contrib/raycast/src/desktop.ts | 29 ++- contrib/raycast/src/error-view.tsx | 55 ++++-- contrib/raycast/src/parse.ts | 3 + contrib/raycast/src/run-command.tsx | 204 +++++++------------ contrib/raycast/src/search-windows.tsx | 102 ++++++---- contrib/raycast/src/search-workspaces.tsx | 79 ++++---- contrib/raycast/src/strip-detail.tsx | 4 +- contrib/raycast/src/suggest.ts | 6 +- contrib/raycast/src/workspace-icon.ts | 55 +++++- 12 files changed, 557 insertions(+), 250 deletions(-) create mode 100644 contrib/raycast/src/commands.ts diff --git a/contrib/raycast/README.md b/contrib/raycast/README.md index 4f138f6e..2ee95433 100644 --- a/contrib/raycast/README.md +++ b/contrib/raycast/README.md @@ -31,27 +31,30 @@ update, run `npm run tinycast` again; there is nothing to reinstall. ## Commands - **Search Windows** finds any window Defi manages by title, app, or - workspace. Enter focuses it, ⇧↵ brings it to the current workspace, ⌘M moves - it to another one, and ⌘⇧F floats or tiles it. Windows you pick often come - first; the focused one comes last. + workspace, and the dropdown narrows it to one workspace. Enter focuses the + window, ⌘↵ moves it to another workspace, ⇧↵ brings it to the current one, + and ⌘⇧F floats or tiles it. Windows you pick often come first; the focused + one comes last. - **Search Workspaces** lists the workspaces of every display with their apps; search by name, number, or app. Enter shows the workspace, ⌘↵ moves the focused window there, ⌘⇧↵ sends it without following, and ⌥↵ moves its column. -- **Run Command** runs any Defi command, with the ones you ran last on top. The +- **Run Command** runs any Defi command, with the ones you ran last on top. + The side pane says what each command does and its default shortcut. The search text also works as an argument: `60` sets the column width to 60%, `+10` or `-10` widens or narrows it, `960` sets it in points, `2` goes to the second workspace, and a workspace name offers to go there or move the focused window or column there. It takes the same text as an argument, so `Run Command 60` works from the root search. ⌘⇧C copies a command for `[keys]`. - See [Commands](../../CONFIGURATION.md#commands). + See [Commands](https://github.com/qeude/Defi/blob/main/CONFIGURATION.md#commands). -In both searches, ⌘Y shows the workspace's strip, and ⌘R refreshes. +In both searches, ⌘Y shows the workspace's strip, and the lists follow Defi live. ## Finding Defi The extension uses Defi.app from `/Applications`, then `~/Applications`. If it lives elsewhere, pick it in **Defi App** in the extension's preferences. -When Defi is not running, Enter launches it. The extension makes no network +When Defi is not running, Enter launches it; when it is not installed, Enter +opens its download page. The extension makes no network calls; window titles stay on your Mac. diff --git a/contrib/raycast/src/commands.ts b/contrib/raycast/src/commands.ts new file mode 100644 index 00000000..01196504 --- /dev/null +++ b/contrib/raycast/src/commands.ts @@ -0,0 +1,231 @@ +import { Icon } from "@raycast/api"; + +export type Entry = { command: string; title: string; icon: Icon; shortcut?: string }; + +// What each command does, from CONFIGURATION.md "Commands". +export const descriptions: Record = { + "focus-column": "Focus the previous, next, first, or last column.", + "focus-window": "Focus another window in the focused column's stack.", + "focus-window-previous": "Go back to the window focused before this one, on any workspace or display.", + "focus-window-or-workspace": + "Like Focus Window, but past the top or bottom of the stack, focus the adjacent workspace.", + "focus-column-or-monitor": "Like Focus Column, but past the first or last column, focus the next display.", + "focus-floating": "Focus another floating window, wrapping around.", + "activate-floating": "Select the floating layer and bring its selected window forward.", + "focus-monitor": "Focus the nearest display in that direction.", + "focus-workspace": "Focus the adjacent workspace on this display, without wrapping.", + "focus-workspace-previous": "Go back to the workspace this display showed before.", + "reorder-workspace": "Move the active workspace up or down its display's stack.", + "move-workspace-to-monitor": "Move the active workspace to another display and remember it there.", + "move-column": "Move the focused column within the strip.", + "move-window": "Move the focused window within its stack.", + "move-column-to-workspace": "Move the focused column to the adjacent workspace and follow it.", + "move-window-to-workspace": "Move only the focused window to the adjacent workspace and follow it.", + "send-column-to-workspace": "Move the focused column to the adjacent workspace without following it.", + "send-window-to-workspace": "Move only the focused window to the adjacent workspace without following it.", + "move-column-to-monitor": "Move the focused column to the nearest display in that direction.", + "move-column-or-to-monitor": "Like Move Column, but past the first or last column, move it to the next display.", + "move-window-to-monitor": "Move only the focused window to the nearest display in that direction.", + "cycle-width": "Step through the preset column widths, wrapping.", + "maximize-column": "Toggle the focused column between full width and its previous width.", + "expand-column-to-available-width": + "Grow the focused column into the free space beside the other fully visible columns.", + "center-column": "Center the focused column until you select another one.", + "center-visible-columns": "Center the fully visible columns as one group.", + "switch-preset-window-height": "Give the focused stacked window the next preset height, wrapping.", + "reset-window-height": "Split the focused stack evenly again.", + "join-window": "Add the focused window to the stack on the left or right.", + "unjoin-windows": "Split the focused window out of its stack into a new column.", + "consume-or-expel-window": + "Alone in its column, the window joins the stack on that side; in a stack, it leaves as a new column.", + "swap-window": "Swap the focused window with the one at the same row of the adjacent column.", + "toggle-floating": "Switch the focused window between tiled and floating.", + "toggle-overview": "Open or close the Overview of every workspace.", + "toggle-cheatsheet": "Open or close the shortcut help.", + "open-settings": "Open Defi's Settings, even with the menu bar item hidden.", + "diagnostic-mark": "Record the current status and recent trace for a bug report, without changing any window.", +}; + +// Argument-free forms of the commands; shortcuts are Defi's built-in defaults. +export const sections: [string, Entry[]][] = [ + [ + "Focus", + [ + { command: "focus-column left", title: "Focus Left Column", icon: Icon.ArrowLeft, shortcut: "mod-left" }, + { command: "focus-column right", title: "Focus Right Column", icon: Icon.ArrowRight, shortcut: "mod-right" }, + { command: "focus-column first", title: "Focus First Column", icon: Icon.ArrowLeftCircle, shortcut: "mod-home" }, + { command: "focus-column last", title: "Focus Last Column", icon: Icon.ArrowRightCircle, shortcut: "mod-end" }, + { command: "focus-window up", title: "Focus Window Above", icon: Icon.ArrowUp, shortcut: "mod-up" }, + { command: "focus-window down", title: "Focus Window Below", icon: Icon.ArrowDown, shortcut: "mod-down" }, + { command: "focus-window first", title: "Focus Top Window", icon: Icon.ArrowUpCircle }, + { command: "focus-window last", title: "Focus Bottom Window", icon: Icon.ArrowDownCircle }, + { command: "focus-window-previous", title: "Focus Previous Window", icon: Icon.Undo }, + { command: "focus-window-or-workspace up", title: "Focus Window or Workspace Above", icon: Icon.ArrowUp }, + { command: "focus-window-or-workspace down", title: "Focus Window or Workspace Below", icon: Icon.ArrowDown }, + { command: "focus-column-or-monitor left", title: "Focus Left Column or Display", icon: Icon.ArrowLeft }, + { command: "focus-column-or-monitor right", title: "Focus Right Column or Display", icon: Icon.ArrowRight }, + { + command: "focus-floating previous", + title: "Focus Previous Floating Window", + icon: Icon.ChevronLeft, + shortcut: "mod-shift-comma", + }, + { + command: "focus-floating next", + title: "Focus Next Floating Window", + icon: Icon.ChevronRight, + shortcut: "mod-shift-period", + }, + { command: "focus-floating first", title: "Focus First Floating Window", icon: Icon.ArrowLeftCircle }, + { command: "focus-floating last", title: "Focus Last Floating Window", icon: Icon.ArrowRightCircle }, + { + command: "activate-floating", + title: "Bring Floating Windows Forward", + icon: Icon.Layers, + shortcut: "mod-shift-v", + }, + ...(["left", "right", "up", "down"] as const).map((direction) => ({ + command: `focus-monitor ${direction}`, + title: `Focus ${displayName(direction)}`, + icon: Icon.Monitor, + shortcut: `mod-shift-${direction}`, + })), + ], + ], + [ + "Workspaces", + [ + { command: "focus-workspace up", title: "Focus Workspace Above", icon: Icon.ChevronUp, shortcut: "mod-i" }, + { command: "focus-workspace down", title: "Focus Workspace Below", icon: Icon.ChevronDown, shortcut: "mod-u" }, + { command: "focus-workspace-previous", title: "Focus Previous Workspace", icon: Icon.Undo }, + { command: "reorder-workspace up", title: "Move Workspace Up", icon: Icon.ArrowUp, shortcut: "mod-shift-i" }, + { + command: "reorder-workspace down", + title: "Move Workspace Down", + icon: Icon.ArrowDown, + shortcut: "mod-shift-u", + }, + ...(["left", "right", "up", "down"] as const).map((direction) => ({ + command: `move-workspace-to-monitor ${direction}`, + title: `Move Workspace to ${displayName(direction)}`, + icon: Icon.Monitor, + })), + ], + ], + [ + "Move", + [ + { command: "move-column left", title: "Move Column Left", icon: Icon.ArrowLeft, shortcut: "mod-cmd-left" }, + { command: "move-column right", title: "Move Column Right", icon: Icon.ArrowRight, shortcut: "mod-cmd-right" }, + { + command: "move-column first", + title: "Move Column to Start", + icon: Icon.ArrowLeftCircle, + shortcut: "mod-cmd-home", + }, + { + command: "move-column last", + title: "Move Column to End", + icon: Icon.ArrowRightCircle, + shortcut: "mod-cmd-end", + }, + { command: "move-window up", title: "Move Window Up", icon: Icon.ArrowUp, shortcut: "mod-cmd-up" }, + { command: "move-window down", title: "Move Window Down", icon: Icon.ArrowDown, shortcut: "mod-cmd-down" }, + { + command: "move-column-to-workspace up", + title: "Move Column to Workspace Above", + icon: Icon.ChevronUp, + shortcut: "mod-cmd-i", + }, + { + command: "move-column-to-workspace down", + title: "Move Column to Workspace Below", + icon: Icon.ChevronDown, + shortcut: "mod-cmd-u", + }, + { command: "move-window-to-workspace up", title: "Move Window to Workspace Above", icon: Icon.ChevronUp }, + { command: "move-window-to-workspace down", title: "Move Window to Workspace Below", icon: Icon.ChevronDown }, + { command: "send-column-to-workspace up", title: "Send Column to Workspace Above", icon: Icon.ArrowNe }, + { command: "send-column-to-workspace down", title: "Send Column to Workspace Below", icon: Icon.ArrowNe }, + { command: "send-window-to-workspace up", title: "Send Window to Workspace Above", icon: Icon.ArrowNe }, + { command: "send-window-to-workspace down", title: "Send Window to Workspace Below", icon: Icon.ArrowNe }, + ...(["left", "right", "up", "down"] as const).map((direction) => ({ + command: `move-column-to-monitor ${direction}`, + title: `Move Column to ${displayName(direction)}`, + icon: Icon.Monitor, + shortcut: `mod-shift-cmd-${direction}`, + })), + { command: "move-column-or-to-monitor left", title: "Move Column Left or to Display", icon: Icon.ArrowLeft }, + { command: "move-column-or-to-monitor right", title: "Move Column Right or to Display", icon: Icon.ArrowRight }, + ...(["left", "right", "up", "down"] as const).map((direction) => ({ + command: `move-window-to-monitor ${direction}`, + title: `Move Window to ${displayName(direction)}`, + icon: Icon.Monitor, + })), + ], + ], + [ + "Size and Stacks", + [ + { command: "cycle-width next", title: "Next Column Width", icon: Icon.ArrowsExpand, shortcut: "mod-equal" }, + { + command: "cycle-width previous", + title: "Previous Column Width", + icon: Icon.ArrowsContract, + shortcut: "mod-minus", + }, + { command: "maximize-column", title: "Maximize Column", icon: Icon.Maximize, shortcut: "mod-f" }, + { command: "expand-column-to-available-width", title: "Fill Available Width", icon: Icon.ArrowsExpand }, + { command: "center-column", title: "Center Column", icon: Icon.Center, shortcut: "mod-c" }, + { command: "center-visible-columns", title: "Center Visible Columns", icon: Icon.AlignCentre }, + { command: "switch-preset-window-height", title: "Next Window Height", icon: Icon.ChevronUpDown }, + { command: "reset-window-height", title: "Reset Window Heights", icon: Icon.RotateAntiClockwise }, + { + command: "join-window left", + title: "Stack into Left Column", + icon: Icon.AppWindowList, + shortcut: "mod-semicolon", + }, + { + command: "join-window right", + title: "Stack into Right Column", + icon: Icon.AppWindowList, + shortcut: "mod-quote", + }, + { command: "unjoin-windows", title: "Unstack Window", icon: Icon.AppWindowGrid2x2, shortcut: "mod-period" }, + { + command: "consume-or-expel-window left", + title: "Stack or Unstack Left", + icon: Icon.AppWindowSidebarLeft, + shortcut: "mod-leftbracket", + }, + { + command: "consume-or-expel-window right", + title: "Stack or Unstack Right", + icon: Icon.AppWindowSidebarRight, + shortcut: "mod-rightbracket", + }, + { command: "swap-window left", title: "Swap with Left Window", icon: Icon.Switch }, + { command: "swap-window right", title: "Swap with Right Window", icon: Icon.Switch }, + { command: "toggle-floating", title: "Float or Tile Window", icon: Icon.Window, shortcut: "mod-v" }, + ], + ], + [ + "Defi", + [ + { command: "toggle-overview", title: "Toggle Overview", icon: Icon.AppWindowGrid3x3, shortcut: "mod-o" }, + { command: "toggle-cheatsheet", title: "Toggle Cheatsheet", icon: Icon.Keyboard, shortcut: "mod-shift-slash" }, + { command: "open-settings", title: "Open Settings", icon: Icon.Gear }, + { command: "diagnostic-mark", title: "Mark a Problem for Diagnostics", icon: Icon.Bug }, + ], + ], +]; + +function cap(word: string) { + return word[0].toUpperCase() + word.slice(1); +} + +function displayName(direction: "left" | "right" | "up" | "down") { + if (direction === "left" || direction === "right") return `${cap(direction)} Display`; + return `Display ${direction === "up" ? "Above" : "Below"}`; +} diff --git a/contrib/raycast/src/defi.ts b/contrib/raycast/src/defi.ts index a202041b..ca9dfa3f 100644 --- a/contrib/raycast/src/defi.ts +++ b/contrib/raycast/src/defi.ts @@ -1,11 +1,11 @@ import { closeMainWindow, getPreferenceValues, showHUD } from "@raycast/api"; -import { showFailureToast } from "@raycast/utils"; -import { execFile } from "node:child_process"; +import { execFile, spawn } from "node:child_process"; import { accessSync, constants } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; export const notRunning = "Defi is not running."; +export const notInstalled = "Defi is not installed."; const cliInApp = "Contents/MacOS/defi"; function isExecutable(path: string): boolean { @@ -33,8 +33,7 @@ export function defiAppPath(): string { const found = ["/Applications/Defi.app", join(homedir(), "Applications/Defi.app")].find((app) => isExecutable(join(app, cliInApp)), ); - if (!found) - throw new Error("Defi is not installed in Applications. Install it, or pick Defi.app in the preferences."); + if (!found) throw new Error(notInstalled); return found; } @@ -49,13 +48,26 @@ export function defi(...args: string[]): Promise { }); } +// Calls onChange whenever Defi reports a change; returns the function that stops listening. +export function subscribe(onChange: () => void): () => void { + try { + const child = spawn(join(defiAppPath(), cliInApp), ["subscribe", "--json"]); + child.on("error", () => undefined); + child.stdout?.on("data", onChange); + return () => child.kill(); + } catch { + return () => undefined; + } +} + // Close Raycast first so the command acts on the window that was focused before it. +// Feedback is a HUD because the launcher window is already gone. export async function runAndClose(commands: string[][], hud?: string): Promise { await closeMainWindow({ clearRootSearch: true }); try { for (const args of commands) await defi(...args); if (hud) await showHUD(hud); } catch (error) { - await showFailureToast(error, { title: "Defi could not do that" }); + await showHUD(`Defi: ${error instanceof Error ? error.message : String(error)}`); } } diff --git a/contrib/raycast/src/desktop.ts b/contrib/raycast/src/desktop.ts index 9abe640d..973d10a6 100644 --- a/contrib/raycast/src/desktop.ts +++ b/contrib/raycast/src/desktop.ts @@ -1,6 +1,7 @@ import { getApplications } from "@raycast/api"; import { useCachedPromise } from "@raycast/utils"; -import { defi } from "./defi"; +import { useEffect, useRef } from "react"; +import { defi, subscribe } from "./defi"; import { parseWindows, parseWorkspaces } from "./parse"; async function readDesktop() { @@ -11,13 +12,31 @@ async function readDesktop() { return { windows, workspaces }; } -// Windows and workspaces read together, shown from the last run while they reload. +async function readApps() { + return (await getApplications()).map(({ bundleId, name, path }) => ({ bundleId, name, path })); +} + +// Windows and workspaces, shown from the last run at once and kept live while the view is open. export function useDesktop() { // The empty view reports the error with its recovery actions, so no toast as well. const desktop = useCachedPromise(readDesktop, [], { onError: () => undefined }); - const apps = useCachedPromise(async () => - (await getApplications()).map(({ bundleId, name, path }) => ({ bundleId, name, path })), - ); + const apps = useCachedPromise(readApps); + const reload = useRef(desktop.revalidate); + reload.current = desktop.revalidate; + + useEffect(() => { + let timer: ReturnType | undefined; + // Defi batches changes within about 30 ms; reload once per burst. + const stop = subscribe(() => { + clearTimeout(timer); + timer = setTimeout(() => reload.current(), 60); + }); + return () => { + clearTimeout(timer); + stop(); + }; + }, []); + const appsByID = new Map(apps.data?.map((app) => [app.bundleId, app])); const windows = desktop.data?.windows; return { diff --git a/contrib/raycast/src/error-view.tsx b/contrib/raycast/src/error-view.tsx index f4f0d748..9c979cee 100644 --- a/contrib/raycast/src/error-view.tsx +++ b/contrib/raycast/src/error-view.tsx @@ -1,21 +1,44 @@ import { Action, ActionPanel, Icon, Keyboard, List, openExtensionPreferences } from "@raycast/api"; -import { defiAppPath, notRunning } from "./defi"; +import { defiAppPath, notInstalled, notRunning } from "./defi"; -function launchTarget(): string | undefined { - try { - return defiAppPath(); - } catch { - return undefined; +const releases = "https://github.com/qeude/Defi/releases/latest"; + +export function describeError(error: Error) { + if (error.message === notRunning) { + return { icon: Icon.Power, title: "Defi Is Not Running", description: "Press Enter to launch it." }; + } + if (error.message === notInstalled) { + return { + icon: Icon.Download, + title: "Defi Is Not Installed", + description: "Download Defi, or pick Defi.app in the extension preferences.", + }; } + return { icon: Icon.Warning, title: "Cannot Reach Defi", description: error.message }; } -// Recovery for every read failure: launch Defi, try again, or fix the app in the preferences. +// Recovery for every read failure: install or launch Defi, try again, or pick the app in the preferences. export function RecoveryActions({ error, onRetry }: { error: Error; onRetry: () => void }) { - const app = error.message === notRunning ? launchTarget() : undefined; + const preferences = ( + + ); + if (error.message === notInstalled) { + return ( + + + {preferences} + + ); + } return ( - {app ? ( - setTimeout(onRetry, 1500)} /> + {error.message === notRunning ? ( + setTimeout(onRetry, 1500)} + /> ) : null} - + {preferences} ); } export function ErrorView({ error, onRetry }: { error: Error; onRetry: () => void }) { - const stopped = error.message === notRunning; - return ( - } - /> - ); + return } />; } diff --git a/contrib/raycast/src/parse.ts b/contrib/raycast/src/parse.ts index 66257c72..dc4957d7 100644 --- a/contrib/raycast/src/parse.ts +++ b/contrib/raycast/src/parse.ts @@ -15,6 +15,8 @@ export type DefiWindow = { export type DefiWorkspace = { id: string; name?: string; + // SF Symbol name set in [workspaces] icons. + icon?: string; position: number; kind: string; display: number; @@ -64,6 +66,7 @@ export function parseWorkspaces(text: string): DefiWorkspace[] { .map((workspace: Record) => ({ id: workspace.id, name: workspace.name, + icon: typeof workspace.icon === "string" ? workspace.icon : undefined, position: workspace.position, kind: String(workspace.kind ?? ""), display: monitor.display, diff --git a/contrib/raycast/src/run-command.tsx b/contrib/raycast/src/run-command.tsx index 096ebd1c..d0e3fe7f 100644 --- a/contrib/raycast/src/run-command.tsx +++ b/contrib/raycast/src/run-command.tsx @@ -1,117 +1,12 @@ import { Action, ActionPanel, Icon, Keyboard, LaunchProps, List } from "@raycast/api"; import { useCachedPromise, useCachedState } from "@raycast/utils"; import { useState } from "react"; -import { defi, notRunning, runAndClose } from "./defi"; -import { RecoveryActions } from "./error-view"; +import { descriptions, sections, type Entry } from "./commands"; +import { defi, runAndClose } from "./defi"; +import { describeError, RecoveryActions } from "./error-view"; import { parseWorkspaces } from "./parse"; import { suggestions, type Suggestion } from "./suggest"; -type Entry = { command: string; title: string; icon: Icon }; - -// Argument-free forms of the commands in CONFIGURATION.md, "Commands". -const sections: [string, Entry[]][] = [ - [ - "Focus", - [ - { command: "focus-column left", title: "Focus Left Column", icon: Icon.ArrowLeft }, - { command: "focus-column right", title: "Focus Right Column", icon: Icon.ArrowRight }, - { command: "focus-column first", title: "Focus First Column", icon: Icon.ArrowLeftCircle }, - { command: "focus-column last", title: "Focus Last Column", icon: Icon.ArrowRightCircle }, - { command: "focus-window up", title: "Focus Window Above", icon: Icon.ArrowUp }, - { command: "focus-window down", title: "Focus Window Below", icon: Icon.ArrowDown }, - { command: "focus-window first", title: "Focus Top Window", icon: Icon.ArrowUpCircle }, - { command: "focus-window last", title: "Focus Bottom Window", icon: Icon.ArrowDownCircle }, - { command: "focus-window-previous", title: "Focus Previous Window", icon: Icon.Undo }, - { command: "focus-window-or-workspace up", title: "Focus Window or Workspace Above", icon: Icon.ArrowUp }, - { command: "focus-window-or-workspace down", title: "Focus Window or Workspace Below", icon: Icon.ArrowDown }, - { command: "focus-column-or-monitor left", title: "Focus Left Column or Display", icon: Icon.ArrowLeft }, - { command: "focus-column-or-monitor right", title: "Focus Right Column or Display", icon: Icon.ArrowRight }, - { command: "focus-floating previous", title: "Focus Previous Floating Window", icon: Icon.ChevronLeft }, - { command: "focus-floating next", title: "Focus Next Floating Window", icon: Icon.ChevronRight }, - { command: "focus-floating first", title: "Focus First Floating Window", icon: Icon.ArrowLeftCircle }, - { command: "focus-floating last", title: "Focus Last Floating Window", icon: Icon.ArrowRightCircle }, - { command: "activate-floating", title: "Bring Floating Windows Forward", icon: Icon.Layers }, - { command: "focus-monitor left", title: "Focus Left Display", icon: Icon.Monitor }, - { command: "focus-monitor right", title: "Focus Right Display", icon: Icon.Monitor }, - { command: "focus-monitor up", title: "Focus Display Above", icon: Icon.Monitor }, - { command: "focus-monitor down", title: "Focus Display Below", icon: Icon.Monitor }, - ], - ], - [ - "Workspaces", - [ - { command: "focus-workspace up", title: "Focus Workspace Above", icon: Icon.ChevronUp }, - { command: "focus-workspace down", title: "Focus Workspace Below", icon: Icon.ChevronDown }, - { command: "focus-workspace-previous", title: "Focus Previous Workspace", icon: Icon.Undo }, - { command: "reorder-workspace up", title: "Move Workspace Up", icon: Icon.ArrowUp }, - { command: "reorder-workspace down", title: "Move Workspace Down", icon: Icon.ArrowDown }, - { command: "move-workspace-to-monitor left", title: "Move Workspace to Left Display", icon: Icon.Monitor }, - { command: "move-workspace-to-monitor right", title: "Move Workspace to Right Display", icon: Icon.Monitor }, - { command: "move-workspace-to-monitor up", title: "Move Workspace to Display Above", icon: Icon.Monitor }, - { command: "move-workspace-to-monitor down", title: "Move Workspace to Display Below", icon: Icon.Monitor }, - ], - ], - [ - "Move", - [ - { command: "move-column left", title: "Move Column Left", icon: Icon.ArrowLeft }, - { command: "move-column right", title: "Move Column Right", icon: Icon.ArrowRight }, - { command: "move-column first", title: "Move Column to Start", icon: Icon.ArrowLeftCircle }, - { command: "move-column last", title: "Move Column to End", icon: Icon.ArrowRightCircle }, - { command: "move-window up", title: "Move Window Up", icon: Icon.ArrowUp }, - { command: "move-window down", title: "Move Window Down", icon: Icon.ArrowDown }, - { command: "move-column-to-workspace up", title: "Move Column to Workspace Above", icon: Icon.ChevronUp }, - { command: "move-column-to-workspace down", title: "Move Column to Workspace Below", icon: Icon.ChevronDown }, - { command: "move-window-to-workspace up", title: "Move Window to Workspace Above", icon: Icon.ChevronUp }, - { command: "move-window-to-workspace down", title: "Move Window to Workspace Below", icon: Icon.ChevronDown }, - { command: "send-column-to-workspace up", title: "Send Column to Workspace Above", icon: Icon.ArrowNe }, - { command: "send-column-to-workspace down", title: "Send Column to Workspace Below", icon: Icon.ArrowNe }, - { command: "send-window-to-workspace up", title: "Send Window to Workspace Above", icon: Icon.ArrowNe }, - { command: "send-window-to-workspace down", title: "Send Window to Workspace Below", icon: Icon.ArrowNe }, - { command: "move-column-or-to-monitor left", title: "Move Column Left or to Display", icon: Icon.ArrowLeft }, - { command: "move-column-or-to-monitor right", title: "Move Column Right or to Display", icon: Icon.ArrowRight }, - { command: "move-column-to-monitor left", title: "Move Column to Left Display", icon: Icon.Monitor }, - { command: "move-column-to-monitor right", title: "Move Column to Right Display", icon: Icon.Monitor }, - { command: "move-column-to-monitor up", title: "Move Column to Display Above", icon: Icon.Monitor }, - { command: "move-column-to-monitor down", title: "Move Column to Display Below", icon: Icon.Monitor }, - { command: "move-window-to-monitor left", title: "Move Window to Left Display", icon: Icon.Monitor }, - { command: "move-window-to-monitor right", title: "Move Window to Right Display", icon: Icon.Monitor }, - { command: "move-window-to-monitor up", title: "Move Window to Display Above", icon: Icon.Monitor }, - { command: "move-window-to-monitor down", title: "Move Window to Display Below", icon: Icon.Monitor }, - ], - ], - [ - "Size and Stacks", - [ - { command: "cycle-width next", title: "Next Column Width", icon: Icon.ArrowsExpand }, - { command: "cycle-width previous", title: "Previous Column Width", icon: Icon.ArrowsContract }, - { command: "maximize-column", title: "Maximize Column", icon: Icon.Maximize }, - { command: "expand-column-to-available-width", title: "Fill Available Width", icon: Icon.ArrowsExpand }, - { command: "center-column", title: "Center Column", icon: Icon.Center }, - { command: "center-visible-columns", title: "Center Visible Columns", icon: Icon.AlignCentre }, - { command: "switch-preset-window-height", title: "Next Window Height", icon: Icon.ChevronUpDown }, - { command: "reset-window-height", title: "Reset Window Heights", icon: Icon.RotateAntiClockwise }, - { command: "join-window left", title: "Stack Into Left Column", icon: Icon.AppWindowList }, - { command: "join-window right", title: "Stack Into Right Column", icon: Icon.AppWindowList }, - { command: "unjoin-windows", title: "Unstack Window", icon: Icon.AppWindowGrid2x2 }, - { command: "consume-or-expel-window left", title: "Stack or Unstack Left", icon: Icon.AppWindowSidebarLeft }, - { command: "consume-or-expel-window right", title: "Stack or Unstack Right", icon: Icon.AppWindowSidebarRight }, - { command: "swap-window left", title: "Swap With Left Window", icon: Icon.Switch }, - { command: "swap-window right", title: "Swap With Right Window", icon: Icon.Switch }, - { command: "toggle-floating", title: "Toggle Floating", icon: Icon.Window }, - ], - ], - [ - "Defi", - [ - { command: "toggle-overview", title: "Toggle Overview", icon: Icon.AppWindowGrid3x3 }, - { command: "toggle-cheatsheet", title: "Toggle Cheatsheet", icon: Icon.Keyboard }, - { command: "open-settings", title: "Open Settings", icon: Icon.Gear }, - { command: "diagnostic-mark", title: "Mark a Problem for Diagnostics", icon: Icon.Bug }, - ], - ], -]; - const entries = sections.flatMap(([section, list]) => list.map((entry) => ({ ...entry, section }))); const recentLimit = 5; @@ -122,12 +17,30 @@ const suggestionIcons: Record = { window: Icon.Window, }; +function CommandDetail({ entry }: { entry: Entry }) { + const description = descriptions[entry.command.split(" ")[0]] ?? ""; + return ( + + + + + } + /> + ); +} + export default function Command(props: LaunchProps<{ arguments: Arguments.RunCommand }>) { - const [searchText, setSearchText] = useState(props.arguments?.query ?? props.fallbackText ?? ""); + // An empty optional argument arrives as "", so fall through to the root-search text. + const [searchText, setSearchText] = useState(props.arguments?.query || props.fallbackText || ""); const [recent, setRecent] = useCachedState("recent-commands", []); + const [showDetail, setShowDetail] = useCachedState("show-command-detail", true); const workspaces = useCachedPromise(async () => parseWorkspaces(await defi("list-workspaces", "--json")), [], { onError: () => undefined, }); + const problem = workspaces.error && describeError(workspaces.error); const suggested = suggestions(searchText, workspaces.data ?? []); const recentEntries = searchText ? [] @@ -138,6 +51,15 @@ export default function Command(props: LaunchProps<{ arguments: Arguments.RunCom return runAndClose([command.split(" ")]); }; + const toggleDetail = ( + setShowDetail(!showDetail)} + /> + ); + const item = (entry: (typeof entries)[number], keyPrefix = "") => ( } actions={ run(entry.command)} /> @@ -153,6 +76,7 @@ export default function Command(props: LaunchProps<{ arguments: Arguments.RunCom content={entry.command} shortcut={Keyboard.Shortcut.Common.Copy} /> + {toggleDetail} } /> @@ -161,41 +85,61 @@ export default function Command(props: LaunchProps<{ arguments: Arguments.RunCom return ( - {workspaces.error?.message === notRunning ? ( + {problem && workspaces.error ? ( } actions={} /> ) : null} {suggested.length > 0 ? ( - {suggested.map((suggestion) => ( - - runAndClose([suggestion.args], suggestion.hud)} + {suggested.map((suggestion) => { + const command = suggestion.args.join(" "); + return ( + + + + } /> - - } - /> - ))} + } + actions={ + + runAndClose([suggestion.args], suggestion.hud)} + /> + + {toggleDetail} + + } + /> + ); + })} ) : null} {recentEntries.length > 0 ? ( diff --git a/contrib/raycast/src/search-windows.tsx b/contrib/raycast/src/search-windows.tsx index 377b7cf7..feed2b7d 100644 --- a/contrib/raycast/src/search-windows.tsx +++ b/contrib/raycast/src/search-windows.tsx @@ -1,5 +1,6 @@ import { Action, ActionPanel, Color, environment, Icon, Keyboard, List } from "@raycast/api"; import { useCachedState, useFrecencySorting } from "@raycast/utils"; +import { useState } from "react"; import { runAndClose } from "./defi"; import { useDesktop } from "./desktop"; import { ErrorView } from "./error-view"; @@ -8,16 +9,22 @@ import { StripDetail } from "./strip-detail"; import { stripColumns } from "./strip"; import { workspaceIcon } from "./workspace-icon"; +const current = "current"; + export default function Command() { const desktop = useDesktop(); const { windows, workspaces, focused, displayCount, appIcon } = desktop; const [showStrip, setShowStrip] = useCachedState("show-strip", false); + const [scope, setScope] = useState(""); // shortcut: ranks windows by picks made here, not by Defi's focus history; use that order once // list-windows exposes it (feat/recent-windows-switcher keeps one). const { data: ranked, visitItem } = useFrecencySorting(windows, { key: (window) => String(window.id) }); + const scopeID = scope === current ? focused?.workspace?.id : scope; + const inScope = ranked.filter((window) => !scopeID || window.workspace?.id === scopeID); // The focused window goes last: Enter on it would change nothing. - const sorted = [...ranked.filter((window) => !window.focused), ...ranked.filter((window) => window.focused)]; + const sorted = [...inScope.filter((window) => !window.focused), ...inScope.filter((window) => window.focused)]; const dark = environment.appearance === "dark"; + const here = focused?.workspace; const position = (window: DefiWindow) => { if (!window.workspace || !window.column) return "Floating"; @@ -27,44 +34,77 @@ export default function Command() { return stack > 1 ? `${place}, row ${window.row} of ${stack}` : place; }; - const focus = async (window: DefiWindow, then: string[][] = [], hud?: string) => { + // shortcut: acting on a window first focuses it, which shows its workspace; a CLI command + // that takes a window ID would avoid that switch. + const act = async (window: DefiWindow, then: string[][] = [], hud?: string) => { await visitItem(window); await runAndClose([["focus-window-id", String(window.id)], ...then], hud); }; + const scopeMenu = ( + + + {here ? : null} + + {workspaces + ?.filter((workspace) => workspace.windowCount > 0) + .map((workspace) => ( + 1 + ? `${workspaceLabel(workspace)} on Display ${workspace.display}` + : workspaceLabel(workspace) + } + value={workspace.id} + icon={workspaceIcon(workspace)} + /> + ))} + + + ); + return ( 0} searchBarPlaceholder="Search windows by title, app, or workspace" + searchBarAccessory={scopeMenu} > {desktop.error ? ( ) : desktop.isLoading ? null : ( )} {sorted.map((window) => { const workspace = window.workspace; const label = workspace ? workspaceLabel(workspace) : undefined; - const here = focused?.workspace; + const fullWorkspace = workspaces?.find((candidate) => candidate.id === workspace?.id); return ( 1 ? [`display ${window.display}`] : []), + ]} accessories={[ ...(window.focused ? [{ tag: { value: "Focused", color: Color.Blue } }] : []), ...(window.floating ? [{ tag: "Floating" }] : []), - ...(workspace && label + ...(fullWorkspace && label ? [ { - icon: workspaceIcon(workspace), + icon: workspaceIcon(fullWorkspace), text: label, tooltip: displayCount > 1 ? `${label} on display ${window.display}` : label, }, @@ -92,28 +132,8 @@ export default function Command() { actions={ - focus(window)} /> - {here && workspace && here.id !== workspace.id ? ( - - focus( - window, - [["move-window-to-workspace-name", here.id]], - `${window.appName} → ${workspaceLabel(here)}`, - ) - } - /> - ) : null} - - - + act(window)} /> + {workspaces ?.filter((target) => target.id !== workspace?.id) .map((target) => { @@ -121,24 +141,38 @@ export default function Command() { return ( 1 ? `${name} on Display ${target.display}` : name} onAction={() => - focus( + act( window, [["move-window-to-workspace-name", target.id]], - `${window.appName} → ${name}`, + `Moved ${window.appName} to ${name}`, ) } /> ); })} + {here && workspace && here.id !== workspace.id ? ( + + act( + window, + [["move-window-to-workspace-name", here.id]], + `Brought ${window.appName} to ${workspaceLabel(here)}`, + ) + } + /> + ) : null} focus(window, [["toggle-floating"]])} + onAction={() => act(window, [["toggle-floating"]])} /> diff --git a/contrib/raycast/src/search-workspaces.tsx b/contrib/raycast/src/search-workspaces.tsx index c1d1756e..dee685ce 100644 --- a/contrib/raycast/src/search-workspaces.tsx +++ b/contrib/raycast/src/search-workspaces.tsx @@ -1,4 +1,4 @@ -import { Action, ActionPanel, Color, environment, Icon, Keyboard, List } from "@raycast/api"; +import { Action, ActionPanel, Color, environment, Icon, Keyboard, List, showHUD } from "@raycast/api"; import { useCachedState } from "@raycast/utils"; import { runAndClose } from "./defi"; import { useDesktop } from "./desktop"; @@ -15,46 +15,45 @@ export default function Command() { const [showStrip, setShowStrip] = useCachedState("show-strip", false); const dark = environment.appearance === "dark"; const displays = [...new Set(workspaces?.map((workspace) => workspace.display))]; - const focusedName = focused?.appName ?? "Window"; + const moving = focused?.appName ?? "Focused Window"; const actions = (workspace: DefiWorkspace, label: string) => { - const display = String(workspace.display); - const elsewhere = focused && focused.workspace?.id !== workspace.id; + const elsewhere = focused !== undefined && focused.workspace?.id !== workspace.id; + // The secondary action keeps ⌘↵ on every row, so it stays even where it has nothing to move. + const moveHere = () => + elsewhere + ? runAndClose([["move-window-to-workspace-name", workspace.id]], `Moved ${moving} to ${label}`) + : showHUD(focused ? `${moving} is already on ${label}` : "No window is focused"); return ( runAndClose([["--monitor", display, "workspace", workspace.id]])} + title={`Go to ${label}`} + icon={Icon.ArrowRight} + onAction={() => runAndClose([["--monitor", String(workspace.display), "workspace", workspace.id]])} /> + + {elsewhere ? ( + <> + + runAndClose([["send-window-to-workspace-name", workspace.id]], `Sent ${moving} to ${label}`) + } + /> + + runAndClose([["move-column-to-workspace-name", workspace.id]], `Moved column to ${label}`) + } + /> + + ) : null} - {elsewhere ? ( - - - runAndClose([["move-window-to-workspace-name", workspace.id]], `${focusedName} → ${label}`) - } - /> - - runAndClose([["send-window-to-workspace-name", workspace.id]], `${focusedName} → ${label}`) - } - /> - runAndClose([["move-column-to-workspace-name", workspace.id]], `Column → ${label}`)} - /> - - ) : null} {desktop.error ? ( - ) : ( - + ) : desktop.isLoading ? null : ( + )} {displays.map((display) => { const onDisplay = workspaces?.filter((workspace) => workspace.display === display) ?? []; @@ -94,15 +93,15 @@ export default function Command() { > {onDisplay.map((workspace) => { const label = workspaceLabel(workspace); - const icons = workspace.applications.flatMap((app) => { - const icon = appIcon(app); - return icon ? [{ icon, tooltip: appName(app) }] : []; - }); + const icons = workspace.applications.map((app) => ({ + icon: appIcon(app) ?? Icon.AppWindow, + tooltip: appName(app), + })); const extra = icons.length - maxAppIcons; return ( )); return ( diff --git a/contrib/raycast/src/suggest.ts b/contrib/raycast/src/suggest.ts index 520f849b..8a04f4d3 100644 --- a/contrib/raycast/src/suggest.ts +++ b/contrib/raycast/src/suggest.ts @@ -19,19 +19,19 @@ function workspaceSuggestions(workspace: DefiWorkspace): Suggestion[] { title: `Move Column to ${label}`, args: ["move-column-to-workspace-name", workspace.id], kind: "column", - hud: `Column → ${label}`, + hud: `Moved column to ${label}`, }, { title: `Move Window to ${label}`, args: ["move-window-to-workspace-name", workspace.id], kind: "window", - hud: `Window → ${label}`, + hud: `Moved window to ${label}`, }, { title: `Send Window to ${label}`, args: ["send-window-to-workspace-name", workspace.id], kind: "window", - hud: `Window → ${label}`, + hud: `Sent window to ${label}`, }, ]; } diff --git a/contrib/raycast/src/workspace-icon.ts b/contrib/raycast/src/workspace-icon.ts index 1b87b7d8..20f46fa6 100644 --- a/contrib/raycast/src/workspace-icon.ts +++ b/contrib/raycast/src/workspace-icon.ts @@ -1,8 +1,55 @@ import { Color, Icon, type Image } from "@raycast/api"; -// The workspace's position as a numbered icon, tinted when it is the active one. -export function workspaceIcon(workspace: { position: number }, active = false): Image.ImageLike { - const name = `Number${String(workspace.position).padStart(2, "0")}` as keyof typeof Icon; - const source = Icon[name] ?? Icon.AppWindowGrid3x3; +// Raycast icons for the SF Symbols most often set in [workspaces] icons. +const symbols: Record = { + curlybraces: Icon.Code, + "chevron.left.forwardslash.chevron.right": Icon.Code, + terminal: Icon.Terminal, + hammer: Icon.Hammer, + "wrench.and.screwdriver": Icon.WrenchScrewdriver, + ladybug: Icon.Bug, + ant: Icon.Bug, + globe: Icon.Globe, + safari: Icon.Globe, + message: Icon.Message, + bubble: Icon.Bubble, + envelope: Icon.Envelope, + calendar: Icon.Calendar, + music: Icon.Music, + headphones: Icon.Headphones, + video: Icon.Video, + photo: Icon.Image, + camera: Icon.Camera, + paintbrush: Icon.Brush, + pencil: Icon.Pencil, + doc: Icon.Document, + book: Icon.Book, + folder: Icon.Folder, + tray: Icon.Tray, + gearshape: Icon.Gear, + gear: Icon.Gear, + cloud: Icon.Cloud, + star: Icon.Star, + heart: Icon.Heart, + house: Icon.House, + person: Icon.Person, + lock: Icon.Lock, + gamecontroller: Icon.GameController, + cart: Icon.Cart, + leaf: Icon.Leaf, + bolt: Icon.Bolt, + airplane: Icon.Airplane, + desktopcomputer: Icon.Desktop, + display: Icon.Monitor, +}; + +type Workspace = { position: number; icon?: string; kind?: string }; + +// The workspace's configured symbol, or its position as a number; tinted when active. +export function workspaceIcon(workspace: Workspace, active = false): Image.ImageLike { + if (workspace.kind === "trailing") return Icon.Plus; + const symbol = workspace.icon?.replace(/\.(fill|circle|square)$/g, ""); + const number = Icon[`Number${String(workspace.position).padStart(2, "0")}` as keyof typeof Icon]; + const source = (symbol && (symbols[symbol] ?? symbols[symbol.split(".")[0]])) || number || Icon.AppWindowGrid3x3; return active ? { source, tintColor: Color.Blue } : source; } From 6c95aa506aaca20497d03d92608640135955fcb7 Mon Sep 17 00:00:00 2001 From: Quentin Eude Date: Wed, 11 Nov 2026 14:50:39 +0100 Subject: [PATCH 08/23] feat(contrib): fix state gaps and teach shortcuts in the Raycast extension Hide cached rows on errors, reconnect the live stream after Defi starts, copy commands in the form [keys] accepts, show default shortcuts as glyphs, offer browsable rows for argument commands, keep the secondary action open when it has nothing to move, remember the window scope, and explain version mismatches. --- contrib/raycast/CHANGELOG.md | 4 +- contrib/raycast/README.md | 90 ++++++++-------- contrib/raycast/package.json | 4 +- contrib/raycast/src/commands.ts | 29 ++++- contrib/raycast/src/defi.ts | 23 ++-- contrib/raycast/src/desktop.ts | 35 ++++-- contrib/raycast/src/error-view.tsx | 11 ++ contrib/raycast/src/parse.ts | 22 ++-- contrib/raycast/src/run-command.tsx | 123 +++++++++++++--------- contrib/raycast/src/search-windows.tsx | 28 +++-- contrib/raycast/src/search-workspaces.tsx | 69 ++++++------ contrib/raycast/src/shortcut.ts | 30 ++++++ contrib/raycast/src/suggest.ts | 59 ++++++++--- contrib/raycast/test/view.test.ts | 24 ++++- 14 files changed, 371 insertions(+), 180 deletions(-) create mode 100644 contrib/raycast/src/shortcut.ts diff --git a/contrib/raycast/CHANGELOG.md b/contrib/raycast/CHANGELOG.md index 5abc5fa2..bc9209e4 100644 --- a/contrib/raycast/CHANGELOG.md +++ b/contrib/raycast/CHANGELOG.md @@ -1,7 +1,7 @@ # Defi Changelog -## [Initial Version] - {PR_MERGE_DATE} +## [Initial Version] - 2026-11-11 - Search Windows: focus any managed window, bring it to the current workspace, move it, float it, or copy its title. - Search Workspaces: jump to a workspace on any display, or move the focused window or column there. -- Run Command: run any Defi command; the search text sets widths and targets workspaces. +- Search Commands: run any Defi command; the search text sets widths and heights and targets workspaces. diff --git a/contrib/raycast/README.md b/contrib/raycast/README.md index 2ee95433..fc31a235 100644 --- a/contrib/raycast/README.md +++ b/contrib/raycast/README.md @@ -1,60 +1,56 @@ # Raycast extension -Reach Defi's windows, workspaces, and commands from -[Raycast](https://www.raycast.com). The extension only runs the `defi` CLI. It -makes no network calls, so window titles stay on your Mac. +Reach [Defi](https://github.com/qeude/Defi), the scrolling-columns window +manager for macOS, from [Raycast](https://www.raycast.com) or +[Tinycast](https://github.com/abue-ammar/tinycast). The extension only runs the +`defi` command line tool inside Defi.app and makes no network calls, so window +titles stay on your Mac. -## Install - -You need Node.js 22 or later. From the Defi repository root: - -```sh -cd contrib/raycast -npm install && npm run dev -``` +## Commands -`npm run dev` adds the extension to Raycast in development mode. +- **Search Windows** finds any window Defi manages by title, app, or + workspace, and the dropdown narrows it to one workspace. Enter focuses the + window, ⌘↵ moves it to another workspace, ⇧↵ brings it to the current one, + and ⌘⇧F floats or tiles it. Windows you pick often come first; the focused + one comes last. +- **Search Workspaces** lists the workspaces of every display with their apps + and strip; search by name, number, or app. Enter goes to the workspace. ⌘↵ + moves the focused window there, ⌘⇧↵ sends it without following, and ⌥↵ and + ⌥⇧↵ do the same with its column. +- **Search Commands** runs any Defi command, with the ones you ran last on top. + The side pane says what each one does, and the row shows its default + shortcut. The search text also works as an argument: `60` sets the column + width to 60%, `+10` or `-10` changes it, `960` sets it in points, `h50` sets a + stacked window's height, `2` goes to the second workspace, and a workspace + name offers to go there or move the focused window or column there. Type the + same text after the command in the root search, as in `Search Commands 60`. + ⌘⇧C copies a command in the form `[keys]` accepts. See + [Commands](https://github.com/qeude/Defi/blob/main/CONFIGURATION.md#commands). + +⌘Y shows or hides the side pane. Lists follow Defi live while they are open. +Shortcuts shown are Defi's defaults, with `mod` as ⌃⌥; your own `[keys]` may +differ. -### Tinycast +## Install -[Tinycast](https://github.com/abue-ammar/tinycast) runs the built extension. -Build it straight into Tinycast's extensions folder: +You need Node.js 22 or later and Defi.app. From the Defi repository root: ```sh cd contrib/raycast -npm install && npm run tinycast +npm install && npm run dev ``` -Tinycast reads each command's bundle when the command starts, so after an -update, run `npm run tinycast` again; there is nothing to reinstall. +`npm run dev` adds the extension to Raycast in development mode. For Tinycast, +run `npm run tinycast` instead: it builds straight into Tinycast's extensions +folder, and Tinycast picks up a new build the next time a command starts. -## Commands +## Troubleshooting -- **Search Windows** finds any window Defi manages by title, app, or - workspace, and the dropdown narrows it to one workspace. Enter focuses the - window, ⌘↵ moves it to another workspace, ⇧↵ brings it to the current one, - and ⌘⇧F floats or tiles it. Windows you pick often come first; the focused - one comes last. -- **Search Workspaces** lists the workspaces of every display with their apps; - search by name, number, or app. Enter shows the workspace, ⌘↵ moves the - focused window there, ⌘⇧↵ sends it without following, and ⌥↵ moves its - column. -- **Run Command** runs any Defi command, with the ones you ran last on top. - The side pane says what each command does and its default shortcut. The - search text also works as an argument: `60` sets the column width to 60%, - `+10` or `-10` widens or narrows it, `960` sets it in points, `2` goes to the - second workspace, and a workspace name offers to go there or move the focused - window or column there. It takes the same text as an argument, so `Run - Command 60` works from the root search. ⌘⇧C copies a command for `[keys]`. - See [Commands](https://github.com/qeude/Defi/blob/main/CONFIGURATION.md#commands). - -In both searches, ⌘Y shows the workspace's strip, and the lists follow Defi live. - -## Finding Defi - -The extension uses Defi.app from `/Applications`, then `~/Applications`. If it -lives elsewhere, pick it in **Defi App** in the extension's preferences. - -When Defi is not running, Enter launches it; when it is not installed, Enter -opens its download page. The extension makes no network -calls; window titles stay on your Mac. +- **Defi Is Not Installed**: the extension looks for Defi.app in + `/Applications`, then `~/Applications`. If it lives elsewhere, pick it in + **Defi App** in the extension's preferences. +- **Defi Is Not Running**: press Enter to launch it; the list fills in once + Defi starts. +- **Update Defi** or **Update This Extension**: Defi and the extension read + different versions of the JSON format. Update the older one; a newer + extension is rebuilt from this folder. diff --git a/contrib/raycast/package.json b/contrib/raycast/package.json index 52092886..792ebf75 100644 --- a/contrib/raycast/package.json +++ b/contrib/raycast/package.json @@ -42,7 +42,7 @@ }, { "name": "run-command", - "title": "Run Command", + "title": "Search Commands", "description": "Run any Defi command; type 60 for a width or a workspace name to go there", "mode": "view", "keywords": [ @@ -57,7 +57,7 @@ { "name": "query", "type": "text", - "placeholder": "60, +10, web", + "placeholder": "Width or workspace", "required": false } ] diff --git a/contrib/raycast/src/commands.ts b/contrib/raycast/src/commands.ts index 01196504..b2d96c79 100644 --- a/contrib/raycast/src/commands.ts +++ b/contrib/raycast/src/commands.ts @@ -2,6 +2,31 @@ import { Icon } from "@raycast/api"; export type Entry = { command: string; title: string; icon: Icon; shortcut?: string }; +// Commands that take a value, offered as rows that put an example in the search bar. +export const prompts: { section: string; title: string; icon: Icon; example: string; hint: string }[] = [ + { + section: "Workspaces", + title: "Go to Workspace…", + icon: Icon.ArrowRight, + example: "1", + hint: "Type a workspace's number or the start of its name.", + }, + { + section: "Widths and Stacks", + title: "Set Column Width…", + icon: Icon.ArrowsExpand, + example: "60", + hint: "Type a width: 60 for 60%, +10 or -10 to change it, or 960 for points.", + }, + { + section: "Widths and Stacks", + title: "Set Window Height…", + icon: Icon.ChevronUpDown, + example: "h50", + hint: "Type h and a height for the focused stacked window: h50, h+10, or h-10.", + }, +]; + // What each command does, from CONFIGURATION.md "Commands". export const descriptions: Record = { "focus-column": "Focus the previous, next, first, or last column.", @@ -165,7 +190,7 @@ export const sections: [string, Entry[]][] = [ ], ], [ - "Size and Stacks", + "Widths and Stacks", [ { command: "cycle-width next", title: "Next Column Width", icon: Icon.ArrowsExpand, shortcut: "mod-equal" }, { @@ -211,7 +236,7 @@ export const sections: [string, Entry[]][] = [ ], ], [ - "Defi", + "Other", [ { command: "toggle-overview", title: "Toggle Overview", icon: Icon.AppWindowGrid3x3, shortcut: "mod-o" }, { command: "toggle-cheatsheet", title: "Toggle Cheatsheet", icon: Icon.Keyboard, shortcut: "mod-shift-slash" }, diff --git a/contrib/raycast/src/defi.ts b/contrib/raycast/src/defi.ts index ca9dfa3f..fa9a5430 100644 --- a/contrib/raycast/src/defi.ts +++ b/contrib/raycast/src/defi.ts @@ -1,4 +1,4 @@ -import { closeMainWindow, getPreferenceValues, showHUD } from "@raycast/api"; +import { closeMainWindow, getPreferenceValues, PopToRootType, showHUD } from "@raycast/api"; import { execFile, spawn } from "node:child_process"; import { accessSync, constants } from "node:fs"; import { homedir } from "node:os"; @@ -48,14 +48,25 @@ export function defi(...args: string[]): Promise { }); } -// Calls onChange whenever Defi reports a change; returns the function that stops listening. -export function subscribe(onChange: () => void): () => void { +// Streams `defi subscribe --json`: onEvent gets each event name, onExit runs when the stream ends. +// Returns the function that stops it. +export function subscribe(onEvent: (event: string) => void, onExit: () => void): () => void { try { const child = spawn(join(defiAppPath(), cliInApp), ["subscribe", "--json"]); + let pending = ""; + child.stdout?.on("data", (chunk: Buffer) => { + const lines = (pending + chunk.toString()).split("\n"); + pending = lines.pop() ?? ""; + for (const line of lines) onEvent(line.match(/"event"\s*:\s*"([^"]+)"/)?.[1] ?? ""); + }); child.on("error", () => undefined); - child.stdout?.on("data", onChange); - return () => child.kill(); + child.on("close", onExit); + return () => { + child.removeAllListeners("close"); + child.kill(); + }; } catch { + onExit(); return () => undefined; } } @@ -63,7 +74,7 @@ export function subscribe(onChange: () => void): () => void { // Close Raycast first so the command acts on the window that was focused before it. // Feedback is a HUD because the launcher window is already gone. export async function runAndClose(commands: string[][], hud?: string): Promise { - await closeMainWindow({ clearRootSearch: true }); + await closeMainWindow({ clearRootSearch: true, popToRootType: PopToRootType.Immediate }); try { for (const args of commands) await defi(...args); if (hud) await showHUD(hud); diff --git a/contrib/raycast/src/desktop.ts b/contrib/raycast/src/desktop.ts index 973d10a6..e1ed152c 100644 --- a/contrib/raycast/src/desktop.ts +++ b/contrib/raycast/src/desktop.ts @@ -25,14 +25,33 @@ export function useDesktop() { reload.current = desktop.revalidate; useEffect(() => { - let timer: ReturnType | undefined; - // Defi batches changes within about 30 ms; reload once per burst. - const stop = subscribe(() => { - clearTimeout(timer); - timer = setTimeout(() => reload.current(), 60); - }); + let stopped = false; + let reloadTimer: ReturnType | undefined; + let retryTimer: ReturnType | undefined; + let stop = () => undefined as void; + let connections = 0; + const start = () => { + connections += 1; + const reconnect = connections > 1; + stop = subscribe( + (event) => { + // The first snapshot repeats what was just read; after a reconnect it is news. + if (event === "snapshot" && !reconnect) return; + // Defi batches changes within about 30 ms; reload once per burst. + clearTimeout(reloadTimer); + reloadTimer = setTimeout(() => reload.current(), 60); + }, + // Defi stopped or is not running yet: keep trying while the view is open. + () => { + if (!stopped) retryTimer = setTimeout(start, 2000); + }, + ); + }; + start(); return () => { - clearTimeout(timer); + stopped = true; + clearTimeout(reloadTimer); + clearTimeout(retryTimer); stop(); }; }, []); @@ -47,6 +66,8 @@ export function useDesktop() { revalidate: desktop.revalidate, displayCount: new Set(desktop.data?.workspaces.map((workspace) => workspace.display)).size, focused: windows?.find((window) => window.focused), + // The workspace the user is on, even when it holds no window. + current: desktop.data?.workspaces.find((workspace) => workspace.active && workspace.focusedDisplay), appIcon: (bundleID?: string) => { const path = bundleID ? appsByID.get(bundleID)?.path : undefined; return path ? { fileIcon: path } : undefined; diff --git a/contrib/raycast/src/error-view.tsx b/contrib/raycast/src/error-view.tsx index 9c979cee..b3346733 100644 --- a/contrib/raycast/src/error-view.tsx +++ b/contrib/raycast/src/error-view.tsx @@ -1,5 +1,6 @@ import { Action, ActionPanel, Icon, Keyboard, List, openExtensionPreferences } from "@raycast/api"; import { defiAppPath, notInstalled, notRunning } from "./defi"; +import { VersionMismatch } from "./parse"; const releases = "https://github.com/qeude/Defi/releases/latest"; @@ -14,6 +15,16 @@ export function describeError(error: Error) { description: "Download Defi, or pick Defi.app in the extension preferences.", }; } + if (error instanceof VersionMismatch) { + return { + icon: Icon.ArrowClockwise, + title: error.received > error.supported ? "Update This Extension" : "Update Defi", + description: + error.received > error.supported + ? "Defi is newer than this extension. Rebuild the extension from the Defi repository." + : "This extension needs a newer Defi. Update Defi from its menu bar item or the releases page.", + }; + } return { icon: Icon.Warning, title: "Cannot Reach Defi", description: error.message }; } diff --git a/contrib/raycast/src/parse.ts b/contrib/raycast/src/parse.ts index dc4957d7..a1bc1d2b 100644 --- a/contrib/raycast/src/parse.ts +++ b/contrib/raycast/src/parse.ts @@ -27,19 +27,25 @@ export type DefiWorkspace = { focusedDisplay: boolean; }; +export class VersionMismatch extends Error { + received: number; + supported: number; + constructor(received: number, supported: number) { + super(`Defi sent format version ${received}; this extension reads version ${supported}.`); + this.received = received; + this.supported = supported; + } +} + // Fields within a version may grow; a new version may break, so it is refused. -function versioned(text: string, version: number, command: string): Record { +function versioned(text: string, version: number): Record { const json = JSON.parse(text); - if (json?.version !== version) { - throw new Error( - `defi ${command} returned format version ${json?.version}; this extension reads version ${version}`, - ); - } + if (json?.version !== version) throw new VersionMismatch(Number(json?.version), version); return json; } export function parseWindows(text: string): DefiWindow[] { - const { windows } = versioned(text, 1, "list-windows --json"); + const { windows } = versioned(text, 1); if (!Array.isArray(windows)) return []; return windows .filter((window) => typeof window?.id === "number") @@ -58,7 +64,7 @@ export function parseWindows(text: string): DefiWindow[] { } export function parseWorkspaces(text: string): DefiWorkspace[] { - const { monitors } = versioned(text, 2, "list-workspaces --json"); + const { monitors } = versioned(text, 2); if (!Array.isArray(monitors)) return []; return monitors.flatMap((monitor) => (Array.isArray(monitor?.workspaces) ? monitor.workspaces : []) diff --git a/contrib/raycast/src/run-command.tsx b/contrib/raycast/src/run-command.tsx index d0e3fe7f..15a727e4 100644 --- a/contrib/raycast/src/run-command.tsx +++ b/contrib/raycast/src/run-command.tsx @@ -1,10 +1,11 @@ import { Action, ActionPanel, Icon, Keyboard, LaunchProps, List } from "@raycast/api"; import { useCachedPromise, useCachedState } from "@raycast/utils"; import { useState } from "react"; -import { descriptions, sections, type Entry } from "./commands"; +import { descriptions, prompts, sections } from "./commands"; import { defi, runAndClose } from "./defi"; import { describeError, RecoveryActions } from "./error-view"; import { parseWorkspaces } from "./parse"; +import { formatShortcut } from "./shortcut"; import { suggestions, type Suggestion } from "./suggest"; const entries = sections.flatMap(([section, list]) => list.map((entry) => ({ ...entry, section }))); @@ -12,21 +13,26 @@ const recentLimit = 5; const suggestionIcons: Record = { width: Icon.ArrowsExpand, + height: Icon.ChevronUpDown, workspace: Icon.ArrowRight, column: Icon.AppWindowSidebarLeft, window: Icon.Window, }; -function CommandDetail({ entry }: { entry: Entry }) { - const description = descriptions[entry.command.split(" ")[0]] ?? ""; +function CommandDetail({ text, command, shortcut }: { text: string; command?: string; shortcut?: string }) { return ( - - - + command ? ( + + + + + ) : undefined } /> ); @@ -40,7 +46,7 @@ export default function Command(props: LaunchProps<{ arguments: Arguments.RunCom const workspaces = useCachedPromise(async () => parseWorkspaces(await defi("list-workspaces", "--json")), [], { onError: () => undefined, }); - const problem = workspaces.error && describeError(workspaces.error); + const problem = workspaces.error; const suggested = suggestions(searchText, workspaces.data ?? []); const recentEntries = searchText ? [] @@ -65,9 +71,16 @@ export default function Command(props: LaunchProps<{ arguments: Arguments.RunCom key={keyPrefix + entry.command} icon={entry.icon} title={entry.title} - subtitle={entry.command} + subtitle={showDetail ? undefined : entry.command} keywords={entry.command.split(/[- ]/)} - detail={} + accessories={entry.shortcut ? [{ text: formatShortcut(entry.shortcut), tooltip: "Default shortcut" }] : []} + detail={ + + } actions={ run(entry.command)} /> @@ -88,58 +101,48 @@ export default function Command(props: LaunchProps<{ arguments: Arguments.RunCom isShowingDetail={showDetail} searchText={searchText} onSearchTextChange={setSearchText} - searchBarPlaceholder="Search commands, or type 60, +10, 2, or a workspace" + searchBarPlaceholder="Search commands, or type 60, +10, 2…" > - {problem && workspaces.error ? ( + {problem ? ( } - actions={} + detail={} + actions={} /> ) : null} {suggested.length > 0 ? ( - {suggested.map((suggestion) => { - const command = suggestion.args.join(" "); - return ( - - - - } + {suggested.map((suggestion) => ( + } + actions={ + + runAndClose([suggestion.args], suggestion.hud)} /> - } - actions={ - - runAndClose([suggestion.args], suggestion.hud)} - /> + {suggestion.binding ? ( - {toggleDetail} - - } - /> - ); - })} + ) : null} + {toggleDetail} + + } + /> + ))} ) : null} {recentEntries.length > 0 ? ( @@ -147,6 +150,28 @@ export default function Command(props: LaunchProps<{ arguments: Arguments.RunCom ) : null} {sections.map(([section]) => ( + {searchText + ? null + : prompts + .filter((prompt) => prompt.section === section) + .map((prompt) => ( + } + actions={ + + setSearchText(prompt.example)} + /> + {toggleDetail} + + } + /> + ))} {entries.filter((entry) => entry.section === section).map((entry) => item(entry))} ))} diff --git a/contrib/raycast/src/search-windows.tsx b/contrib/raycast/src/search-windows.tsx index feed2b7d..33b92218 100644 --- a/contrib/raycast/src/search-windows.tsx +++ b/contrib/raycast/src/search-windows.tsx @@ -1,4 +1,4 @@ -import { Action, ActionPanel, Color, environment, Icon, Keyboard, List } from "@raycast/api"; +import { Action, ActionPanel, Color, environment, Icon, Keyboard, LaunchProps, List } from "@raycast/api"; import { useCachedState, useFrecencySorting } from "@raycast/utils"; import { useState } from "react"; import { runAndClose } from "./defi"; @@ -11,20 +11,23 @@ import { workspaceIcon } from "./workspace-icon"; const current = "current"; -export default function Command() { +export default function Command(props: LaunchProps) { const desktop = useDesktop(); - const { windows, workspaces, focused, displayCount, appIcon } = desktop; - const [showStrip, setShowStrip] = useCachedState("show-strip", false); - const [scope, setScope] = useState(""); - // shortcut: ranks windows by picks made here, not by Defi's focus history; use that order once - // list-windows exposes it (feat/recent-windows-switcher keeps one). + const { windows, workspaces, displayCount, appIcon, current: here } = desktop; + const [searchText, setSearchText] = useState(props.fallbackText ?? ""); + const [showStrip, setShowStrip] = useCachedState("windows-strip", false); + const [storedScope, setScope] = useCachedState("window-scope", ""); + // A remembered workspace that has since gone falls back to all workspaces. + const scope = storedScope === current || workspaces?.some((w) => w.id === storedScope) ? storedScope : ""; + // shortcut: ranks windows by picks made here, not by Defi's focus history, and keeps the ranks of + // closed windows; use Defi's order once list-windows exposes it (feat/recent-windows-switcher keeps one). const { data: ranked, visitItem } = useFrecencySorting(windows, { key: (window) => String(window.id) }); - const scopeID = scope === current ? focused?.workspace?.id : scope; - const inScope = ranked.filter((window) => !scopeID || window.workspace?.id === scopeID); + const scopeID = scope === current ? here?.id : scope; + // Cached rows would look live while Defi is unreachable, so an error hides them. + const inScope = desktop.error ? [] : ranked.filter((window) => !scopeID || window.workspace?.id === scopeID); // The focused window goes last: Enter on it would change nothing. const sorted = [...inScope.filter((window) => !window.focused), ...inScope.filter((window) => window.focused)]; const dark = environment.appearance === "dark"; - const here = focused?.workspace; const position = (window: DefiWindow) => { if (!window.workspace || !window.column) return "Floating"; @@ -70,6 +73,9 @@ export default function Command() { isShowingDetail={showStrip && sorted.length > 0} searchBarPlaceholder="Search windows by title, app, or workspace" searchBarAccessory={scopeMenu} + searchText={searchText} + onSearchTextChange={setSearchText} + filtering > {desktop.error ? ( @@ -106,7 +112,7 @@ export default function Command() { { icon: workspaceIcon(fullWorkspace), text: label, - tooltip: displayCount > 1 ? `${label} on display ${window.display}` : label, + tooltip: displayCount > 1 ? `${label} on Display ${window.display}` : label, }, ] : []), diff --git a/contrib/raycast/src/search-workspaces.tsx b/contrib/raycast/src/search-workspaces.tsx index dee685ce..1be7b332 100644 --- a/contrib/raycast/src/search-workspaces.tsx +++ b/contrib/raycast/src/search-workspaces.tsx @@ -1,4 +1,4 @@ -import { Action, ActionPanel, Color, environment, Icon, Keyboard, List, showHUD } from "@raycast/api"; +import { Action, ActionPanel, Color, environment, Icon, Keyboard, List, showToast, Toast } from "@raycast/api"; import { useCachedState } from "@raycast/utils"; import { runAndClose } from "./defi"; import { useDesktop } from "./desktop"; @@ -12,18 +12,22 @@ const maxAppIcons = 5; export default function Command() { const desktop = useDesktop(); const { windows, workspaces, focused, displayCount, appIcon, appName } = desktop; - const [showStrip, setShowStrip] = useCachedState("show-strip", false); + const [showStrip, setShowStrip] = useCachedState("workspaces-strip", true); const dark = environment.appearance === "dark"; - const displays = [...new Set(workspaces?.map((workspace) => workspace.display))]; - const moving = focused?.appName ?? "Focused Window"; + // Cached rows would look live while Defi is unreachable, so an error hides them. + const shown = desktop.error ? [] : (workspaces ?? []); + const displays = [...new Set(shown.map((workspace) => workspace.display))]; + const moving = focused?.appName ?? "window"; const actions = (workspace: DefiWorkspace, label: string) => { const elsewhere = focused !== undefined && focused.workspace?.id !== workspace.id; - // The secondary action keeps ⌘↵ on every row, so it stays even where it has nothing to move. - const moveHere = () => + const move = (command: string, hud: string) => () => elsewhere - ? runAndClose([["move-window-to-workspace-name", workspace.id]], `Moved ${moving} to ${label}`) - : showHUD(focused ? `${moving} is already on ${label}` : "No window is focused"); + ? runAndClose([[command, workspace.id]], hud) + : showToast({ + style: Toast.Style.Failure, + title: focused ? `${moving} is already on ${label}` : "No window is focused", + }); return ( @@ -32,27 +36,30 @@ export default function Command() { icon={Icon.ArrowRight} onAction={() => runAndClose([["--monitor", String(workspace.display), "workspace", workspace.id]])} /> - - {elsewhere ? ( - <> - - runAndClose([["send-window-to-workspace-name", workspace.id]], `Sent ${moving} to ${label}`) - } - /> - - runAndClose([["move-column-to-workspace-name", workspace.id]], `Moved column to ${label}`) - } - /> - - ) : null} + {/* The secondary action keeps ⌘↵ on every row, so it stays even where it has nothing to move. */} + + + + 0} searchBarPlaceholder="Search workspaces by name, number, or app" > {desktop.error ? ( @@ -84,7 +91,7 @@ export default function Command() { )} {displays.map((display) => { - const onDisplay = workspaces?.filter((workspace) => workspace.display === display) ?? []; + const onDisplay = shown.filter((workspace) => workspace.display === display); return ( = { ctrl: "⌃", alt: "⌥", shift: "⇧", cmd: "⌘" }; +const keys: Record = { + left: "←", + right: "→", + up: "↑", + down: "↓", + home: "↖", + end: "↘", + pageup: "⇞", + pagedown: "⇟", + comma: ",", + period: ".", + slash: "/", + semicolon: ";", + quote: "'", + leftbracket: "[", + rightbracket: "]", + minus: "-", + equal: "=", + tab: "⇥", +}; + +// "mod-shift-comma" as macOS glyphs with Defi's default mod (ctrl+alt): "⌃⌥⇧,". +export function formatShortcut(shortcut: string): string { + const parts = shortcut.split("-"); + const key = parts.pop() ?? ""; + const held = new Set(parts.flatMap((part) => (part === "mod" ? ["ctrl", "alt"] : [part]))); + const glyphs = ["ctrl", "alt", "shift", "cmd"].filter((name) => held.has(name)).map((name) => modifiers[name]); + return glyphs.join("") + (keys[key] ?? key.toUpperCase()); +} diff --git a/contrib/raycast/src/suggest.ts b/contrib/raycast/src/suggest.ts index 8a04f4d3..527fa477 100644 --- a/contrib/raycast/src/suggest.ts +++ b/contrib/raycast/src/suggest.ts @@ -2,41 +2,72 @@ import { workspaceLabel, type DefiWorkspace } from "./parse.ts"; export type Suggestion = { title: string; + // What runs; `binding` is the same command in the form [keys] accepts, when there is one. args: string[]; - kind: "width" | "workspace" | "column" | "window"; + binding?: string; + kind: "width" | "height" | "workspace" | "column" | "window"; hud?: string; }; +// Go to, then the four moves in the order Search Workspaces offers them. function workspaceSuggestions(workspace: DefiWorkspace): Suggestion[] { const label = workspaceLabel(workspace); + const named = workspace.name !== undefined; + const target = (byName: string, byPosition?: string) => + named ? `${byName} ${workspace.name}` : byPosition && `${byPosition} ${workspace.position}`; return [ { title: `Go to ${label}`, args: ["--monitor", String(workspace.display), "workspace", workspace.id], + binding: target("workspace", "focus-workspace-position"), kind: "workspace", }, - { - title: `Move Column to ${label}`, - args: ["move-column-to-workspace-name", workspace.id], - kind: "column", - hud: `Moved column to ${label}`, - }, { title: `Move Window to ${label}`, args: ["move-window-to-workspace-name", workspace.id], + binding: target("move-window-to-workspace-name", "move-window-to-workspace-position"), kind: "window", hud: `Moved window to ${label}`, }, { title: `Send Window to ${label}`, args: ["send-window-to-workspace-name", workspace.id], + binding: target("send-window-to-workspace-name", "send-window-to-workspace-position"), kind: "window", hud: `Sent window to ${label}`, }, + { + title: `Move Column to ${label}`, + args: ["move-column-to-workspace-name", workspace.id], + binding: target("move-column-to-workspace-name", "move-column-to-workspace-position"), + kind: "column", + hud: `Moved column to ${label}`, + }, + { + title: `Send Column to ${label}`, + args: ["send-column-to-workspace-name", workspace.id], + binding: target("send-column-to-workspace-name"), + kind: "column", + hud: `Sent column to ${label}`, + }, ]; } -// Commands whose argument is the search text: "60" sets a width, "+10" widens, "web" or "2" targets a workspace. +function sized(kind: "width" | "height", sign: string | undefined, amount: number, points: boolean): Suggestion { + const value = `${sign ?? ""}${amount}${points ? "" : "%"}`; + const shown = points ? `${amount} pt` : `${amount}%`; + const noun = kind === "width" ? "Column Width" : "Window Height"; + const title = !sign + ? `Set ${noun} to ${shown}` + : kind === "width" + ? `${sign === "+" ? "Widen" : "Narrow"} Column by ${shown}` + : `${sign === "+" ? "Grow" : "Shrink"} Window by ${shown}`; + const command = kind === "width" ? "set-column-width" : "set-window-height"; + return { title, args: [command, value], binding: `${command} ${value}`, kind, hud: title }; +} + +// Commands whose argument is the search text: "60" sets a width, "h50" a stacked window's height, +// "+10" widens, "2" or "web" targets a workspace. export function suggestions(query: string, workspaces: DefiWorkspace[]): Suggestion[] { const text = query.trim(); if (!text) return []; @@ -49,13 +80,13 @@ export function suggestions(query: string, workspaces: DefiWorkspace[]): Suggest const byPosition = !sign && !unit && workspaces.find((w) => w.focusedDisplay && w.position === amount); if (byPosition) result.push(workspaceSuggestions(byPosition)[0]); const points = unit ? unit !== "%" : amount > 100; - const shown = points ? `${amount} pt` : `${amount}%`; // Defi clamps a width to at least 5% of the usable width. - if (sign ? amount > 0 : points || amount >= 5) { - const value = `${sign ?? ""}${amount}${points ? "" : "%"}`; - const title = !sign ? `Set Column Width to ${shown}` : `${sign === "+" ? "Widen" : "Narrow"} Column by ${shown}`; - result.push({ title, args: ["set-column-width", value], kind: "width", hud: title }); - } + if (sign ? amount > 0 : points || amount >= 5) result.push(sized("width", sign, amount, points)); + } + + const height = text.match(/^h\s*([+-])?(\d{1,3})\s*%?$/i); + if (height && Number(height[2]) > 0 && Number(height[2]) <= 100) { + result.push(sized("height", height[1], Number(height[2]), false)); } const lowered = text.toLowerCase(); diff --git a/contrib/raycast/test/view.test.ts b/contrib/raycast/test/view.test.ts index 0a2d64eb..a9d54633 100644 --- a/contrib/raycast/test/view.test.ts +++ b/contrib/raycast/test/view.test.ts @@ -3,6 +3,7 @@ import { readFileSync } from "node:fs"; import { test } from "node:test"; import { parseWindows, parseWorkspaces } from "../src/parse.ts"; import { stripColumns, stripImage } from "../src/strip.ts"; +import { formatShortcut } from "../src/shortcut.ts"; import { suggestions } from "../src/suggest.ts"; const fixture = (name: string) => readFileSync(new URL(`fixtures/${name}`, import.meta.url), "utf8"); @@ -38,12 +39,33 @@ test("search text becomes a command argument", () => { assert.deepEqual(titles("+10"), [["Widen Column by 10%", "set-column-width +10%"]]); assert.deepEqual(titles("-40pt"), [["Narrow Column by 40 pt", "set-column-width -40"]]); assert.deepEqual(titles("960"), [["Set Column Width to 960 pt", "set-column-width 960"]]); + assert.deepEqual(titles("h50"), [["Set Window Height to 50%", "set-window-height 50%"]]); + assert.deepEqual(titles("h-10"), [["Shrink Window by 10%", "set-window-height -10%"]]); assert.deepEqual(titles("We"), [ ["Go to web", "--monitor 1 workspace web"], - ["Move Column to web", "move-column-to-workspace-name web"], ["Move Window to web", "move-window-to-workspace-name web"], ["Send Window to web", "send-window-to-workspace-name web"], + ["Move Column to web", "move-column-to-workspace-name web"], + ["Send Column to web", "send-column-to-workspace-name web"], ]); assert.deepEqual(titles("zzz"), []); assert.deepEqual(titles(" "), []); }); + +test("copied commands use the form [keys] accepts", () => { + const bindings = (query: string) => suggestions(query, workspaces).map((s) => s.binding); + assert.deepEqual(bindings("web").slice(0, 2), ["workspace web", "move-window-to-workspace-name web"]); + assert.deepEqual(bindings("5"), ["focus-workspace-position 5", "set-column-width 5%"]); + const unnamed = { ...workspaces[4], kind: "ordinary" }; + assert.deepEqual( + suggestions("5", [unnamed]).map((s) => s.binding), + ["focus-workspace-position 5", "set-column-width 5%"], + ); +}); + +test("shortcuts read as macOS glyphs with the default mod", () => { + assert.equal(formatShortcut("mod-left"), "⌃⌥←"); + assert.equal(formatShortcut("mod-shift-comma"), "⌃⌥⇧,"); + assert.equal(formatShortcut("mod-shift-cmd-up"), "⌃⌥⇧⌘↑"); + assert.equal(formatShortcut("mod-f"), "⌃⌥F"); +}); From decc825244b0b96bd5fdd52c9b1112d81ca89fa8 Mon Sep 17 00:00:00 2001 From: Quentin Eude Date: Wed, 11 Nov 2026 14:59:49 +0100 Subject: [PATCH 09/23] feat(contrib): open workspaces' windows and tighten the Raycast extension Add Show Windows to Search Workspaces, keep argument rows findable while searching, name each suggestion's default shortcut and effect, keep rows bare beside the side pane, word-match workspace names, word HUDs as results, and drop the live stream: launchers do not reliably run effect cleanups, which left defi subscribe running. --- contrib/raycast/README.md | 9 +- contrib/raycast/src/commands.ts | 41 +++- contrib/raycast/src/defi.ts | 28 +-- contrib/raycast/src/desktop.ts | 41 +--- contrib/raycast/src/error-view.tsx | 11 +- contrib/raycast/src/run-command.tsx | 68 +++--- contrib/raycast/src/search-windows.tsx | 217 +------------------ contrib/raycast/src/search-workspaces.tsx | 56 +++-- contrib/raycast/src/strip-detail.tsx | 19 +- contrib/raycast/src/suggest.ts | 69 ++++-- contrib/raycast/src/window-list.tsx | 247 ++++++++++++++++++++++ contrib/raycast/test/view.test.ts | 34 ++- 12 files changed, 471 insertions(+), 369 deletions(-) create mode 100644 contrib/raycast/src/window-list.tsx diff --git a/contrib/raycast/README.md b/contrib/raycast/README.md index fc31a235..2ac80c9b 100644 --- a/contrib/raycast/README.md +++ b/contrib/raycast/README.md @@ -12,11 +12,12 @@ titles stay on your Mac. workspace, and the dropdown narrows it to one workspace. Enter focuses the window, ⌘↵ moves it to another workspace, ⇧↵ brings it to the current one, and ⌘⇧F floats or tiles it. Windows you pick often come first; the focused - one comes last. + one comes last. Acting on a window from another workspace shows that + workspace for a moment, because Defi focuses the window first. - **Search Workspaces** lists the workspaces of every display with their apps and strip; search by name, number, or app. Enter goes to the workspace. ⌘↵ moves the focused window there, ⌘⇧↵ sends it without following, and ⌥↵ and - ⌥⇧↵ do the same with its column. + ⌥⇧↵ do the same with its column. ⌘O lists the workspace's windows. - **Search Commands** runs any Defi command, with the ones you ran last on top. The side pane says what each one does, and the row shows its default shortcut. The search text also works as an argument: `60` sets the column @@ -24,10 +25,10 @@ titles stay on your Mac. stacked window's height, `2` goes to the second workspace, and a workspace name offers to go there or move the focused window or column there. Type the same text after the command in the root search, as in `Search Commands 60`. - ⌘⇧C copies a command in the form `[keys]` accepts. See + ⌘↵ copies a command in the form `[keys]` accepts. See [Commands](https://github.com/qeude/Defi/blob/main/CONFIGURATION.md#commands). -⌘Y shows or hides the side pane. Lists follow Defi live while they are open. +⌘Y shows or hides the side pane, and ⌘R reloads a list. Shortcuts shown are Defi's defaults, with `mod` as ⌃⌥; your own `[keys]` may differ. diff --git a/contrib/raycast/src/commands.ts b/contrib/raycast/src/commands.ts index b2d96c79..71405a4a 100644 --- a/contrib/raycast/src/commands.ts +++ b/contrib/raycast/src/commands.ts @@ -1,15 +1,23 @@ import { Icon } from "@raycast/api"; -export type Entry = { command: string; title: string; icon: Icon; shortcut?: string }; +export type Entry = { command: string; title: string; icon: Icon; shortcut?: string; hud?: string }; // Commands that take a value, offered as rows that put an example in the search bar. -export const prompts: { section: string; title: string; icon: Icon; example: string; hint: string }[] = [ +export const prompts: { + section: string; + title: string; + icon: Icon; + example: string; + hint: string; + keywords: string[]; +}[] = [ { section: "Workspaces", title: "Go to Workspace…", icon: Icon.ArrowRight, example: "1", - hint: "Type a workspace's number or the start of its name.", + hint: "Type a workspace's number or the start of its name, such as 2 or web.", + keywords: ["workspace", "go", "switch", "number", "name"], }, { section: "Widths and Stacks", @@ -17,6 +25,7 @@ export const prompts: { section: string; title: string; icon: Icon; example: str icon: Icon.ArrowsExpand, example: "60", hint: "Type a width: 60 for 60%, +10 or -10 to change it, or 960 for points.", + keywords: ["width", "resize", "size", "column"], }, { section: "Widths and Stacks", @@ -24,6 +33,7 @@ export const prompts: { section: string; title: string; icon: Icon; example: str icon: Icon.ChevronUpDown, example: "h50", hint: "Type h and a height for the focused stacked window: h50, h+10, or h-10.", + keywords: ["height", "stack", "resize", "size"], }, ]; @@ -87,8 +97,8 @@ export const sections: [string, Entry[]][] = [ { command: "focus-window-previous", title: "Focus Previous Window", icon: Icon.Undo }, { command: "focus-window-or-workspace up", title: "Focus Window or Workspace Above", icon: Icon.ArrowUp }, { command: "focus-window-or-workspace down", title: "Focus Window or Workspace Below", icon: Icon.ArrowDown }, - { command: "focus-column-or-monitor left", title: "Focus Left Column or Display", icon: Icon.ArrowLeft }, - { command: "focus-column-or-monitor right", title: "Focus Right Column or Display", icon: Icon.ArrowRight }, + { command: "focus-column-or-monitor left", title: "Focus Left Column or Left Display", icon: Icon.ArrowLeft }, + { command: "focus-column-or-monitor right", title: "Focus Right Column or Right Display", icon: Icon.ArrowRight }, { command: "focus-floating previous", title: "Focus Previous Floating Window", @@ -120,9 +130,9 @@ export const sections: [string, Entry[]][] = [ [ "Workspaces", [ - { command: "focus-workspace up", title: "Focus Workspace Above", icon: Icon.ChevronUp, shortcut: "mod-i" }, - { command: "focus-workspace down", title: "Focus Workspace Below", icon: Icon.ChevronDown, shortcut: "mod-u" }, - { command: "focus-workspace-previous", title: "Focus Previous Workspace", icon: Icon.Undo }, + { command: "focus-workspace up", title: "Go to Workspace Above", icon: Icon.ChevronUp, shortcut: "mod-i" }, + { command: "focus-workspace down", title: "Go to Workspace Below", icon: Icon.ChevronDown, shortcut: "mod-u" }, + { command: "focus-workspace-previous", title: "Go to Previous Workspace", icon: Icon.Undo }, { command: "reorder-workspace up", title: "Move Workspace Up", icon: Icon.ArrowUp, shortcut: "mod-shift-i" }, { command: "reorder-workspace down", @@ -180,8 +190,12 @@ export const sections: [string, Entry[]][] = [ icon: Icon.Monitor, shortcut: `mod-shift-cmd-${direction}`, })), - { command: "move-column-or-to-monitor left", title: "Move Column Left or to Display", icon: Icon.ArrowLeft }, - { command: "move-column-or-to-monitor right", title: "Move Column Right or to Display", icon: Icon.ArrowRight }, + { command: "move-column-or-to-monitor left", title: "Move Column Left or to Left Display", icon: Icon.ArrowLeft }, + { + command: "move-column-or-to-monitor right", + title: "Move Column Right or to Right Display", + icon: Icon.ArrowRight, + }, ...(["left", "right", "up", "down"] as const).map((direction) => ({ command: `move-window-to-monitor ${direction}`, title: `Move Window to ${displayName(direction)}`, @@ -241,7 +255,12 @@ export const sections: [string, Entry[]][] = [ { command: "toggle-overview", title: "Toggle Overview", icon: Icon.AppWindowGrid3x3, shortcut: "mod-o" }, { command: "toggle-cheatsheet", title: "Toggle Cheatsheet", icon: Icon.Keyboard, shortcut: "mod-shift-slash" }, { command: "open-settings", title: "Open Settings", icon: Icon.Gear }, - { command: "diagnostic-mark", title: "Mark a Problem for Diagnostics", icon: Icon.Bug }, + { + command: "diagnostic-mark", + title: "Mark a Problem for Diagnostics", + icon: Icon.Bug, + hud: "Marked for diagnostics", + }, ], ], ]; diff --git a/contrib/raycast/src/defi.ts b/contrib/raycast/src/defi.ts index fa9a5430..3be69ea6 100644 --- a/contrib/raycast/src/defi.ts +++ b/contrib/raycast/src/defi.ts @@ -1,5 +1,5 @@ import { closeMainWindow, getPreferenceValues, PopToRootType, showHUD } from "@raycast/api"; -import { execFile, spawn } from "node:child_process"; +import { execFile } from "node:child_process"; import { accessSync, constants } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; @@ -48,29 +48,6 @@ export function defi(...args: string[]): Promise { }); } -// Streams `defi subscribe --json`: onEvent gets each event name, onExit runs when the stream ends. -// Returns the function that stops it. -export function subscribe(onEvent: (event: string) => void, onExit: () => void): () => void { - try { - const child = spawn(join(defiAppPath(), cliInApp), ["subscribe", "--json"]); - let pending = ""; - child.stdout?.on("data", (chunk: Buffer) => { - const lines = (pending + chunk.toString()).split("\n"); - pending = lines.pop() ?? ""; - for (const line of lines) onEvent(line.match(/"event"\s*:\s*"([^"]+)"/)?.[1] ?? ""); - }); - child.on("error", () => undefined); - child.on("close", onExit); - return () => { - child.removeAllListeners("close"); - child.kill(); - }; - } catch { - onExit(); - return () => undefined; - } -} - // Close Raycast first so the command acts on the window that was focused before it. // Feedback is a HUD because the launcher window is already gone. export async function runAndClose(commands: string[][], hud?: string): Promise { @@ -79,6 +56,7 @@ export async function runAndClose(commands: string[][], hud?: string): Promise ({ bundleId, name, path })); } -// Windows and workspaces, shown from the last run at once and kept live while the view is open. +// Windows and workspaces, shown from the last run at once while they reload. +// No live stream: launchers do not always run effect cleanups, which would leave `defi subscribe` running. export function useDesktop() { // The empty view reports the error with its recovery actions, so no toast as well. const desktop = useCachedPromise(readDesktop, [], { onError: () => undefined }); const apps = useCachedPromise(readApps); - const reload = useRef(desktop.revalidate); - reload.current = desktop.revalidate; - - useEffect(() => { - let stopped = false; - let reloadTimer: ReturnType | undefined; - let retryTimer: ReturnType | undefined; - let stop = () => undefined as void; - let connections = 0; - const start = () => { - connections += 1; - const reconnect = connections > 1; - stop = subscribe( - (event) => { - // The first snapshot repeats what was just read; after a reconnect it is news. - if (event === "snapshot" && !reconnect) return; - // Defi batches changes within about 30 ms; reload once per burst. - clearTimeout(reloadTimer); - reloadTimer = setTimeout(() => reload.current(), 60); - }, - // Defi stopped or is not running yet: keep trying while the view is open. - () => { - if (!stopped) retryTimer = setTimeout(start, 2000); - }, - ); - }; - start(); - return () => { - stopped = true; - clearTimeout(reloadTimer); - clearTimeout(retryTimer); - stop(); - }; - }, []); - const appsByID = new Map(apps.data?.map((app) => [app.bundleId, app])); const windows = desktop.data?.windows; return { diff --git a/contrib/raycast/src/error-view.tsx b/contrib/raycast/src/error-view.tsx index b3346733..c5633456 100644 --- a/contrib/raycast/src/error-view.tsx +++ b/contrib/raycast/src/error-view.tsx @@ -1,4 +1,4 @@ -import { Action, ActionPanel, Icon, Keyboard, List, openExtensionPreferences } from "@raycast/api"; +import { Action, ActionPanel, Icon, List, openExtensionPreferences } from "@raycast/api"; import { defiAppPath, notInstalled, notRunning } from "./defi"; import { VersionMismatch } from "./parse"; @@ -48,15 +48,10 @@ export function RecoveryActions({ error, onRetry }: { error: Error; onRetry: () title="Launch Defi" icon={Icon.Power} target={defiAppPath()} - onOpen={() => setTimeout(onRetry, 1500)} + onOpen={() => [1000, 2500, 5000].forEach((delay) => setTimeout(onRetry, delay))} /> ) : null} - + {preferences} ); diff --git a/contrib/raycast/src/run-command.tsx b/contrib/raycast/src/run-command.tsx index 15a727e4..5e840833 100644 --- a/contrib/raycast/src/run-command.tsx +++ b/contrib/raycast/src/run-command.tsx @@ -1,10 +1,10 @@ import { Action, ActionPanel, Icon, Keyboard, LaunchProps, List } from "@raycast/api"; -import { useCachedPromise, useCachedState } from "@raycast/utils"; +import { useCachedState } from "@raycast/utils"; import { useState } from "react"; import { descriptions, prompts, sections } from "./commands"; -import { defi, runAndClose } from "./defi"; +import { runAndClose } from "./defi"; +import { useDesktop } from "./desktop"; import { describeError, RecoveryActions } from "./error-view"; -import { parseWorkspaces } from "./parse"; import { formatShortcut } from "./shortcut"; import { suggestions, type Suggestion } from "./suggest"; @@ -19,7 +19,8 @@ const suggestionIcons: Record = { window: Icon.Window, }; -function CommandDetail({ text, command, shortcut }: { text: string; command?: string; shortcut?: string }) { +// What a row does; Default Shortcut appears where Defi binds one or for a command that has none. +function CommandDetail({ text, command, shortcut }: { text: string; command?: string; shortcut?: string | null }) { return ( - + {shortcut !== undefined ? ( + + ) : null} ) : undefined } @@ -43,18 +46,16 @@ export default function Command(props: LaunchProps<{ arguments: Arguments.RunCom const [searchText, setSearchText] = useState(props.arguments?.query || props.fallbackText || ""); const [recent, setRecent] = useCachedState("recent-commands", []); const [showDetail, setShowDetail] = useCachedState("show-command-detail", true); - const workspaces = useCachedPromise(async () => parseWorkspaces(await defi("list-workspaces", "--json")), [], { - onError: () => undefined, - }); - const problem = workspaces.error; - const suggested = suggestions(searchText, workspaces.data ?? []); + const desktop = useDesktop(); + const problem = desktop.error; + const suggested = suggestions(searchText, problem ? [] : (desktop.workspaces ?? [])); const recentEntries = searchText ? [] : recent.flatMap((command) => entries.filter((entry) => entry.command === command)); - const run = (command: string) => { - setRecent([command, ...recent.filter((other) => other !== command)].slice(0, recentLimit)); - return runAndClose([command.split(" ")]); + const run = (entry: (typeof entries)[number]) => { + setRecent([entry.command, ...recent.filter((other) => other !== entry.command)].slice(0, recentLimit)); + return runAndClose([entry.command.split(" ")], entry.hud); }; const toggleDetail = ( @@ -73,22 +74,21 @@ export default function Command(props: LaunchProps<{ arguments: Arguments.RunCom title={entry.title} subtitle={showDetail ? undefined : entry.command} keywords={entry.command.split(/[- ]/)} - accessories={entry.shortcut ? [{ text: formatShortcut(entry.shortcut), tooltip: "Default shortcut" }] : []} + // The side pane shows the shortcut, so rows next to it stay bare. + accessories={ + !showDetail && entry.shortcut ? [{ text: formatShortcut(entry.shortcut), tooltip: "Default shortcut" }] : [] + } detail={ } actions={ - run(entry.command)} /> - + run(entry)} /> + {toggleDetail} } @@ -110,7 +110,7 @@ export default function Command(props: LaunchProps<{ arguments: Arguments.RunCom title={describeError(problem).title} keywords={[searchText]} detail={} - actions={} + actions={} /> ) : null} @@ -123,7 +123,13 @@ export default function Command(props: LaunchProps<{ arguments: Arguments.RunCom title={suggestion.title} // Built-in filtering would drop "+10" from "Widen Column by 10%"; the query always matches. keywords={[searchText]} - detail={} + detail={ + + } actions={ runAndClose([suggestion.args], suggestion.hud)} /> {suggestion.binding ? ( - + ) : null} {toggleDetail} @@ -150,7 +152,8 @@ export default function Command(props: LaunchProps<{ arguments: Arguments.RunCom ) : null} {sections.map(([section]) => ( - {searchText + {/* Example rows stay findable by search, and step aside once the search is itself an argument. */} + {suggested.length > 0 ? null : prompts .filter((prompt) => prompt.section === section) @@ -159,6 +162,7 @@ export default function Command(props: LaunchProps<{ arguments: Arguments.RunCom key={prompt.title} icon={prompt.icon} title={prompt.title} + keywords={prompt.keywords} detail={} actions={ diff --git a/contrib/raycast/src/search-windows.tsx b/contrib/raycast/src/search-windows.tsx index 33b92218..f85bf8be 100644 --- a/contrib/raycast/src/search-windows.tsx +++ b/contrib/raycast/src/search-windows.tsx @@ -1,217 +1,6 @@ -import { Action, ActionPanel, Color, environment, Icon, Keyboard, LaunchProps, List } from "@raycast/api"; -import { useCachedState, useFrecencySorting } from "@raycast/utils"; -import { useState } from "react"; -import { runAndClose } from "./defi"; -import { useDesktop } from "./desktop"; -import { ErrorView } from "./error-view"; -import { workspaceLabel, type DefiWindow } from "./parse"; -import { StripDetail } from "./strip-detail"; -import { stripColumns } from "./strip"; -import { workspaceIcon } from "./workspace-icon"; - -const current = "current"; +import { LaunchProps } from "@raycast/api"; +import { WindowList } from "./window-list"; export default function Command(props: LaunchProps) { - const desktop = useDesktop(); - const { windows, workspaces, displayCount, appIcon, current: here } = desktop; - const [searchText, setSearchText] = useState(props.fallbackText ?? ""); - const [showStrip, setShowStrip] = useCachedState("windows-strip", false); - const [storedScope, setScope] = useCachedState("window-scope", ""); - // A remembered workspace that has since gone falls back to all workspaces. - const scope = storedScope === current || workspaces?.some((w) => w.id === storedScope) ? storedScope : ""; - // shortcut: ranks windows by picks made here, not by Defi's focus history, and keeps the ranks of - // closed windows; use Defi's order once list-windows exposes it (feat/recent-windows-switcher keeps one). - const { data: ranked, visitItem } = useFrecencySorting(windows, { key: (window) => String(window.id) }); - const scopeID = scope === current ? here?.id : scope; - // Cached rows would look live while Defi is unreachable, so an error hides them. - const inScope = desktop.error ? [] : ranked.filter((window) => !scopeID || window.workspace?.id === scopeID); - // The focused window goes last: Enter on it would change nothing. - const sorted = [...inScope.filter((window) => !window.focused), ...inScope.filter((window) => window.focused)]; - const dark = environment.appearance === "dark"; - - const position = (window: DefiWindow) => { - if (!window.workspace || !window.column) return "Floating"; - const columns = stripColumns(windows ?? [], window.workspace.id); - const stack = columns[window.column - 1]?.length ?? 1; - const place = `Column ${window.column} of ${columns.length}`; - return stack > 1 ? `${place}, row ${window.row} of ${stack}` : place; - }; - - // shortcut: acting on a window first focuses it, which shows its workspace; a CLI command - // that takes a window ID would avoid that switch. - const act = async (window: DefiWindow, then: string[][] = [], hud?: string) => { - await visitItem(window); - await runAndClose([["focus-window-id", String(window.id)], ...then], hud); - }; - - const scopeMenu = ( - - - {here ? : null} - - {workspaces - ?.filter((workspace) => workspace.windowCount > 0) - .map((workspace) => ( - 1 - ? `${workspaceLabel(workspace)} on Display ${workspace.display}` - : workspaceLabel(workspace) - } - value={workspace.id} - icon={workspaceIcon(workspace)} - /> - ))} - - - ); - - return ( - 0} - searchBarPlaceholder="Search windows by title, app, or workspace" - searchBarAccessory={scopeMenu} - searchText={searchText} - onSearchTextChange={setSearchText} - filtering - > - {desktop.error ? ( - - ) : desktop.isLoading ? null : ( - - )} - {sorted.map((window) => { - const workspace = window.workspace; - const label = workspace ? workspaceLabel(workspace) : undefined; - const fullWorkspace = workspaces?.find((candidate) => candidate.id === workspace?.id); - return ( - 1 ? [`display ${window.display}`] : []), - ]} - accessories={[ - ...(window.focused ? [{ tag: { value: "Focused", color: Color.Blue } }] : []), - ...(window.floating ? [{ tag: "Floating" }] : []), - ...(fullWorkspace && label - ? [ - { - icon: workspaceIcon(fullWorkspace), - text: label, - tooltip: displayCount > 1 ? `${label} on Display ${window.display}` : label, - }, - ] - : []), - ]} - detail={ - workspace ? ( - - - {displayCount > 1 ? ( - - ) : null} - - - - ) : undefined - } - actions={ - - - act(window)} /> - - {workspaces - ?.filter((target) => target.id !== workspace?.id) - .map((target) => { - const name = workspaceLabel(target); - return ( - 1 ? `${name} on Display ${target.display}` : name} - onAction={() => - act( - window, - [["move-window-to-workspace-name", target.id]], - `Moved ${window.appName} to ${name}`, - ) - } - /> - ); - })} - - {here && workspace && here.id !== workspace.id ? ( - - act( - window, - [["move-window-to-workspace-name", here.id]], - `Brought ${window.appName} to ${workspaceLabel(here)}`, - ) - } - /> - ) : null} - act(window, [["toggle-floating"]])} - /> - - - - - - - setShowStrip(!showStrip)} - /> - - - - } - /> - ); - })} - - ); + return ; } diff --git a/contrib/raycast/src/search-workspaces.tsx b/contrib/raycast/src/search-workspaces.tsx index 1be7b332..b5a9905a 100644 --- a/contrib/raycast/src/search-workspaces.tsx +++ b/contrib/raycast/src/search-workspaces.tsx @@ -1,18 +1,32 @@ -import { Action, ActionPanel, Color, environment, Icon, Keyboard, List, showToast, Toast } from "@raycast/api"; +import { + Action, + ActionPanel, + Color, + environment, + Icon, + Keyboard, + LaunchProps, + List, + showToast, + Toast, +} from "@raycast/api"; import { useCachedState } from "@raycast/utils"; +import { useState } from "react"; import { runAndClose } from "./defi"; import { useDesktop } from "./desktop"; import { ErrorView } from "./error-view"; import { workspaceLabel, type DefiWorkspace } from "./parse"; import { StripDetail } from "./strip-detail"; +import { WindowList } from "./window-list"; import { workspaceIcon } from "./workspace-icon"; const maxAppIcons = 5; -export default function Command() { +export default function Command(props: LaunchProps) { const desktop = useDesktop(); const { windows, workspaces, focused, displayCount, appIcon, appName } = desktop; - const [showStrip, setShowStrip] = useCachedState("workspaces-strip", true); + const [searchText, setSearchText] = useState(props.fallbackText ?? ""); + const [showDetail, setShowDetail] = useCachedState("workspaces-strip", true); const dark = environment.appearance === "dark"; // Cached rows would look live while Defi is unreachable, so an error hides them. const shown = desktop.error ? [] : (workspaces ?? []); @@ -24,10 +38,7 @@ export default function Command() { const move = (command: string, hud: string) => () => elsewhere ? runAndClose([[command, workspace.id]], hud) - : showToast({ - style: Toast.Style.Failure, - title: focused ? `${moving} is already on ${label}` : "No window is focused", - }); + : showToast({ style: Toast.Style.Failure, title: focused ? `Already on ${label}` : "No Focused Window" }); return ( @@ -62,11 +73,19 @@ export default function Command() { /> + {workspace.windowCount > 0 ? ( + } + /> + ) : null} setShowStrip(!showStrip)} + onAction={() => setShowDetail(!showDetail)} /> 0} + isShowingDetail={showDetail && shown.length > 0} searchBarPlaceholder="Search workspaces by name, number, or app" + searchText={searchText} + onSearchTextChange={setSearchText} + filtering > {desktop.error ? ( @@ -105,17 +127,19 @@ export default function Command() { tooltip: appName(app), })); const extra = icons.length - maxAppIcons; + const activeTag = workspace.active ? [{ tag: { value: "Active", color: Color.Blue } }] : []; return ( 0 ? [{ text: `+${extra}` }] : []), - ...(workspace.active ? [{ tag: { value: "Active", color: Color.Blue } }] : []), - ]} + // The side pane names every app, so rows next to it keep only the active tag. + accessories={ + showDetail + ? activeTag + : [...icons.slice(0, maxAppIcons), ...(extra > 0 ? [{ text: `+${extra}` }] : []), ...activeTag] + } detail={ 1 ? ( ) : null} - } actions={actions(workspace, label)} diff --git a/contrib/raycast/src/strip-detail.tsx b/contrib/raycast/src/strip-detail.tsx index 4ff7ef53..45fa5c26 100644 --- a/contrib/raycast/src/strip-detail.tsx +++ b/contrib/raycast/src/strip-detail.tsx @@ -8,28 +8,39 @@ type Props = { highlightID?: number; dark: boolean; appIcon: (bundleID?: string) => Image.ImageLike | undefined; + empty: string; children?: React.ReactNode; }; -// The workspace's strip with its columns named in order, under the rows the caller adds first. -export function StripDetail({ windows, workspaceID, highlightID, dark, appIcon, children }: Props) { +const maxTagLength = 24; + +// The workspace's strip, then the caller's rows, then its columns named in order. +export function StripDetail({ windows, workspaceID, highlightID, dark, appIcon, empty, children }: Props) { const columns = stripColumns(windows, workspaceID); const floating = windows.filter((window) => window.workspace?.id === workspaceID && window.floating); + const onWorkspace = [...columns.flat(), ...floating]; + // When one app fills several columns, its name tells them apart no more; its titles do. + const repeated = (window: DefiWindow) => onWorkspace.filter((other) => other.appID === window.appID).length > 1; + const name = (window: DefiWindow) => { + if (!repeated(window) || !window.title) return window.appName; + return window.title.length > maxTagLength ? `${window.title.slice(0, maxTagLength - 1)}…` : window.title; + }; const tags = (list: DefiWindow[]) => list.map((window) => ( )); return ( {children} + {onWorkspace.length > 0 ? : null} {columns.map((column, index) => ( {tags(column)} diff --git a/contrib/raycast/src/suggest.ts b/contrib/raycast/src/suggest.ts index 527fa477..6a107a71 100644 --- a/contrib/raycast/src/suggest.ts +++ b/contrib/raycast/src/suggest.ts @@ -2,9 +2,12 @@ import { workspaceLabel, type DefiWorkspace } from "./parse.ts"; export type Suggestion = { title: string; + description: string; // What runs; `binding` is the same command in the form [keys] accepts, when there is one. args: string[]; binding?: string; + // Defi's built-in shortcut for it, in [keys] syntax, when it has one. + shortcut?: string; kind: "width" | "height" | "workspace" | "column" | "window"; hud?: string; }; @@ -15,15 +18,20 @@ function workspaceSuggestions(workspace: DefiWorkspace): Suggestion[] { const named = workspace.name !== undefined; const target = (byName: string, byPosition?: string) => named ? `${byName} ${workspace.name}` : byPosition && `${byPosition} ${workspace.position}`; + // mod-1 … mod-9 reach the first nine workspaces of the focused display. + const digit = workspace.focusedDisplay && workspace.position <= 9 ? String(workspace.position) : undefined; return [ { title: `Go to ${label}`, + description: `Shows ${label} on its display.`, args: ["--monitor", String(workspace.display), "workspace", workspace.id], binding: target("workspace", "focus-workspace-position"), + shortcut: digit && `mod-${digit}`, kind: "workspace", }, { title: `Move Window to ${label}`, + description: `Moves the focused window to ${label} and follows it.`, args: ["move-window-to-workspace-name", workspace.id], binding: target("move-window-to-workspace-name", "move-window-to-workspace-position"), kind: "window", @@ -31,6 +39,7 @@ function workspaceSuggestions(workspace: DefiWorkspace): Suggestion[] { }, { title: `Send Window to ${label}`, + description: `Moves the focused window to ${label} and stays here.`, args: ["send-window-to-workspace-name", workspace.id], binding: target("send-window-to-workspace-name", "send-window-to-workspace-position"), kind: "window", @@ -38,13 +47,16 @@ function workspaceSuggestions(workspace: DefiWorkspace): Suggestion[] { }, { title: `Move Column to ${label}`, + description: `Moves the focused column to ${label} and follows it.`, args: ["move-column-to-workspace-name", workspace.id], binding: target("move-column-to-workspace-name", "move-column-to-workspace-position"), + shortcut: digit && `mod-cmd-${digit}`, kind: "column", hud: `Moved column to ${label}`, }, { title: `Send Column to ${label}`, + description: `Moves the focused column to ${label} and stays here.`, args: ["send-column-to-workspace-name", workspace.id], binding: target("send-column-to-workspace-name"), kind: "column", @@ -53,17 +65,44 @@ function workspaceSuggestions(workspace: DefiWorkspace): Suggestion[] { ]; } -function sized(kind: "width" | "height", sign: string | undefined, amount: number, points: boolean): Suggestion { +const verbs = { + width: { "+": ["Widen", "Widened", "Widens"], "-": ["Narrow", "Narrowed", "Narrows"] }, + height: { "+": ["Grow", "Grew", "Grows"], "-": ["Shrink", "Shrank", "Shrinks"] }, +} as const; + +function sized(kind: "width" | "height", sign: "+" | "-" | undefined, amount: number, points: boolean): Suggestion { const value = `${sign ?? ""}${amount}${points ? "" : "%"}`; const shown = points ? `${amount} pt` : `${amount}%`; - const noun = kind === "width" ? "Column Width" : "Window Height"; - const title = !sign - ? `Set ${noun} to ${shown}` - : kind === "width" - ? `${sign === "+" ? "Widen" : "Narrow"} Column by ${shown}` - : `${sign === "+" ? "Grow" : "Shrink"} Window by ${shown}`; - const command = kind === "width" ? "set-column-width" : "set-window-height"; - return { title, args: [command, value], binding: `${command} ${value}`, kind, hud: title }; + const width = kind === "width"; + const subject = width ? "column" : "window"; + const whole = width ? "the usable width" : "the column height"; + const command = width ? "set-column-width" : "set-window-height"; + const fields = { args: [command, value], binding: `${command} ${value}`, kind }; + if (!sign) { + const noun = width ? "Column Width" : "Window Height"; + return { + ...fields, + title: `Set ${noun} to ${shown}`, + description: points + ? `Makes the focused ${subject} ${amount} points wide.` + : `Sets the focused ${width ? "column" : "stacked window"} to ${amount}% of ${whole}.`, + hud: `${width ? "Column width" : "Window height"} ${shown}`, + }; + } + const [verb, past, third] = verbs[kind][sign]; + return { + ...fields, + title: `${verb} ${width ? "Column" : "Window"} by ${shown}`, + description: `${third} the focused ${width ? "column" : "stacked window"} by ${points ? `${amount} points` : `${amount}% of ${whole}`}.`, + hud: `${past} ${subject} by ${shown}`, + }; +} + +// A workspace matches when the search starts its name or one of its words: "sec" finds dev-secondary. +function matchesName(name: string, text: string): boolean { + const lowered = text.toLowerCase(); + const words = name.toLowerCase().split(/[-_.\s]+/); + return name.toLowerCase().startsWith(lowered) || words.some((word) => word.startsWith(lowered)); } // Commands whose argument is the search text: "60" sets a width, "h50" a stacked window's height, @@ -78,20 +117,22 @@ export function suggestions(query: string, workspaces: DefiWorkspace[]): Suggest const [, sign, digits, unit] = width; const amount = Number(digits); const byPosition = !sign && !unit && workspaces.find((w) => w.focusedDisplay && w.position === amount); - if (byPosition) result.push(workspaceSuggestions(byPosition)[0]); const points = unit ? unit !== "%" : amount > 100; + const forWorkspace = byPosition ? workspaceSuggestions(byPosition) : []; + result.push(...forWorkspace.slice(0, 1)); // Defi clamps a width to at least 5% of the usable width. - if (sign ? amount > 0 : points || amount >= 5) result.push(sized("width", sign, amount, points)); + if (sign ? amount > 0 : points || amount >= 5) + result.push(sized("width", sign as "+" | "-" | undefined, amount, points)); + result.push(...forWorkspace.slice(1)); } const height = text.match(/^h\s*([+-])?(\d{1,3})\s*%?$/i); if (height && Number(height[2]) > 0 && Number(height[2]) <= 100) { - result.push(sized("height", height[1], Number(height[2]), false)); + result.push(sized("height", height[1] as "+" | "-" | undefined, Number(height[2]), false)); } - const lowered = text.toLowerCase(); for (const workspace of workspaces) { - if (workspace.name?.toLowerCase().startsWith(lowered)) result.push(...workspaceSuggestions(workspace)); + if (workspace.name && matchesName(workspace.name, text)) result.push(...workspaceSuggestions(workspace)); } return result; } diff --git a/contrib/raycast/src/window-list.tsx b/contrib/raycast/src/window-list.tsx new file mode 100644 index 00000000..0c4a917b --- /dev/null +++ b/contrib/raycast/src/window-list.tsx @@ -0,0 +1,247 @@ +import { Action, ActionPanel, Color, environment, Icon, Keyboard, List } from "@raycast/api"; +import { useCachedState, useFrecencySorting } from "@raycast/utils"; +import { useState } from "react"; +import { runAndClose } from "./defi"; +import { useDesktop } from "./desktop"; +import { ErrorView } from "./error-view"; +import { workspaceLabel, type DefiWindow } from "./parse"; +import { StripDetail } from "./strip-detail"; +import { stripColumns } from "./strip"; +import { workspaceIcon } from "./workspace-icon"; + +const current = "current"; + +// The window switcher; with workspaceID it lists that workspace only, as Search Workspaces opens it. +export function WindowList({ initialSearch = "", workspaceID }: { initialSearch?: string; workspaceID?: string }) { + const desktop = useDesktop(); + const { windows, workspaces, displayCount, appIcon, current: here } = desktop; + const [searchText, setSearchText] = useState(initialSearch); + const [showDetail, setShowDetail] = useCachedState("windows-strip", false); + const [storedScope, setScope] = useCachedState("window-scope", ""); + const withWindows = workspaces?.filter((workspace) => workspace.windowCount > 0) ?? []; + // A remembered workspace that has since emptied or gone falls back to all workspaces. + const scope = + workspaceID ?? (storedScope === current || withWindows.some((w) => w.id === storedScope) ? storedScope : ""); + // shortcut: ranks windows by picks made here, not by Defi's focus history, and keeps the ranks of + // closed windows; use Defi's order once list-windows exposes it (feat/recent-windows-switcher keeps one). + const { data: ranked, visitItem } = useFrecencySorting(windows, { key: (window) => String(window.id) }); + const scopeID = scope === current ? here?.id : scope; + const scopeName = workspaces?.find((workspace) => workspace.id === scopeID); + // Cached rows would look live while Defi is unreachable, so an error hides them. + const inScope = desktop.error ? [] : ranked.filter((window) => !scopeID || window.workspace?.id === scopeID); + // The focused window goes last: Enter on it would change nothing. + const sorted = [...inScope.filter((window) => !window.focused), ...inScope.filter((window) => window.focused)]; + const dark = environment.appearance === "dark"; + + const position = (window: DefiWindow) => { + if (!window.workspace || !window.column) return "Floating"; + const columns = stripColumns(windows ?? [], window.workspace.id); + const stack = columns[window.column - 1]?.length ?? 1; + const place = `Column ${window.column} of ${columns.length}`; + return stack > 1 ? `${place}, row ${window.row} of ${stack}` : place; + }; + + // shortcut: acting on a window first focuses it, which shows its workspace; a CLI command + // that takes a window ID would avoid that switch. + const act = async (window: DefiWindow, then: string[][] = [], hud?: string) => { + await visitItem(window); + await runAndClose([["focus-window-id", String(window.id)], ...then], hud); + }; + + const scopeMenu = workspaceID ? undefined : ( + + + {here ? : null} + + {withWindows.map((workspace) => ( + 1 + ? `${workspaceLabel(workspace)} on Display ${workspace.display}` + : workspaceLabel(workspace) + } + value={workspace.id} + icon={workspaceIcon(workspace)} + /> + ))} + + + ); + + const emptyView = () => { + if (desktop.error) return ; + if (desktop.isLoading) return null; + if (scopeID && !searchText) { + const label = scopeName ? workspaceLabel(scopeName) : "This Workspace"; + return ( + + setScope("")} /> + + ) + } + /> + ); + } + return ( + + ); + }; + + return ( + 0} + navigationTitle={workspaceID && scopeName ? `Windows on ${workspaceLabel(scopeName)}` : undefined} + searchBarPlaceholder="Search windows by title, app, or workspace" + searchBarAccessory={scopeMenu} + searchText={searchText} + onSearchTextChange={setSearchText} + filtering + > + {emptyView()} + {sorted.map((window) => { + const workspace = window.workspace; + const label = workspace ? workspaceLabel(workspace) : undefined; + const fullWorkspace = workspaces?.find((candidate) => candidate.id === workspace?.id); + const focusedTag = window.focused ? [{ tag: { value: "Focused", color: Color.Blue } }] : []; + return ( + 1 ? [`display ${window.display}`] : []), + ]} + // The side pane repeats the rest, so rows next to it keep only the focus tag. + accessories={ + showDetail + ? focusedTag + : [ + ...focusedTag, + ...(window.floating ? [{ tag: "Floating" }] : []), + ...(fullWorkspace && label + ? [ + { + icon: workspaceIcon(fullWorkspace), + text: label, + tooltip: displayCount > 1 ? `${label} on Display ${window.display}` : label, + }, + ] + : []), + ] + } + detail={ + workspace ? ( + + + + {displayCount > 1 ? ( + + ) : null} + + + ) : undefined + } + actions={ + + + act(window)} /> + + {workspaces + ?.filter((target) => target.id !== workspace?.id) + .map((target) => { + const name = workspaceLabel(target); + return ( + 1 ? `${name} on Display ${target.display}` : name} + onAction={() => + act( + window, + [["move-window-to-workspace-name", target.id]], + `Moved ${window.appName} to ${name}`, + ) + } + /> + ); + })} + + {here && workspace && here.id !== workspace.id ? ( + + act( + window, + [["move-window-to-workspace-name", here.id]], + `Brought ${window.appName} to ${workspaceLabel(here)}`, + ) + } + /> + ) : null} + + act(window, [["toggle-floating"]], `${window.floating ? "Tiled" : "Floated"} ${window.appName}`) + } + /> + + + + + + + setShowDetail(!showDetail)} + /> + + + + } + /> + ); + })} + + ); +} diff --git a/contrib/raycast/test/view.test.ts b/contrib/raycast/test/view.test.ts index a9d54633..df9061f0 100644 --- a/contrib/raycast/test/view.test.ts +++ b/contrib/raycast/test/view.test.ts @@ -34,7 +34,14 @@ test("the strip image draws one tile per tiled window and highlights one", () => test("search text becomes a command argument", () => { const titles = (query: string) => suggestions(query, workspaces).map((s) => [s.title, s.args.join(" ")]); assert.deepEqual(titles("60"), [["Set Column Width to 60%", "set-column-width 60%"]]); - assert.deepEqual(titles("3"), [["Go to dev-secondary", "--monitor 1 workspace dev-secondary"]]); + assert.deepEqual(titles("3"), [ + ["Go to dev-secondary", "--monitor 1 workspace dev-secondary"], + ["Move Window to dev-secondary", "move-window-to-workspace-name dev-secondary"], + ["Send Window to dev-secondary", "send-window-to-workspace-name dev-secondary"], + ["Move Column to dev-secondary", "move-column-to-workspace-name dev-secondary"], + ["Send Column to dev-secondary", "send-column-to-workspace-name dev-secondary"], + ]); + assert.deepEqual(titles("sec").map(([title]) => title)[0], "Go to dev-secondary"); assert.deepEqual(titles("7"), [["Set Column Width to 7%", "set-column-width 7%"]]); assert.deepEqual(titles("+10"), [["Widen Column by 10%", "set-column-width +10%"]]); assert.deepEqual(titles("-40pt"), [["Narrow Column by 40 pt", "set-column-width -40"]]); @@ -55,11 +62,32 @@ test("search text becomes a command argument", () => { test("copied commands use the form [keys] accepts", () => { const bindings = (query: string) => suggestions(query, workspaces).map((s) => s.binding); assert.deepEqual(bindings("web").slice(0, 2), ["workspace web", "move-window-to-workspace-name web"]); - assert.deepEqual(bindings("5"), ["focus-workspace-position 5", "set-column-width 5%"]); + assert.deepEqual(bindings("5").slice(0, 2), ["focus-workspace-position 5", "set-column-width 5%"]); const unnamed = { ...workspaces[4], kind: "ordinary" }; assert.deepEqual( suggestions("5", [unnamed]).map((s) => s.binding), - ["focus-workspace-position 5", "set-column-width 5%"], + [ + "focus-workspace-position 5", + "set-column-width 5%", + "move-window-to-workspace-position 5", + "send-window-to-workspace-position 5", + "move-column-to-workspace-position 5", + undefined, + ], + ); + assert.deepEqual( + suggestions("2", workspaces) + .map((s) => s.shortcut) + .filter(Boolean), + ["mod-2", "mod-cmd-2"], + ); + assert.deepEqual( + suggestions("+10", workspaces).map((s) => s.hud), + ["Widened column by 10%"], + ); + assert.deepEqual( + suggestions("h-10", workspaces).map((s) => s.hud), + ["Shrank window by 10%"], ); }); From a1b3011fd6295fd438ffed31c8a4dd406e6caf14 Mon Sep 17 00:00:00 2001 From: Quentin Eude Date: Wed, 11 Nov 2026 15:06:44 +0100 Subject: [PATCH 10/23] fix(contrib): act on fresh state and keep the Raycast extension honest Show digit shortcuts only where Defi binds them, check a fresh read before moving, keep the launcher open while Defi launches, give every Search Commands row a copy as its secondary action, trim suggestions for short queries, start each launch on all workspaces, keep rows bare next to the side pane, and test the catalog against CONFIGURATION.md. --- contrib/raycast/src/commands.ts | 153 +++++++++++----------- contrib/raycast/src/defi.ts | 3 +- contrib/raycast/src/desktop.ts | 4 +- contrib/raycast/src/error-view.tsx | 33 +++-- contrib/raycast/src/run-command.tsx | 15 ++- contrib/raycast/src/search-workspaces.tsx | 39 ++++-- contrib/raycast/src/suggest.ts | 32 +++-- contrib/raycast/src/window-list.tsx | 37 ++++-- contrib/raycast/test/commands.test.ts | 39 ++++++ contrib/raycast/test/view.test.ts | 46 +++---- 10 files changed, 247 insertions(+), 154 deletions(-) create mode 100644 contrib/raycast/test/commands.test.ts diff --git a/contrib/raycast/src/commands.ts b/contrib/raycast/src/commands.ts index 71405a4a..c89f58ee 100644 --- a/contrib/raycast/src/commands.ts +++ b/contrib/raycast/src/commands.ts @@ -1,37 +1,44 @@ -import { Icon } from "@raycast/api"; +import type { Icon } from "@raycast/api"; -export type Entry = { command: string; title: string; icon: Icon; shortcut?: string; hud?: string }; +type IconName = keyof typeof Icon; + +export type Entry = { command: string; title: string; icon: IconName; shortcut?: string; hud?: string }; // Commands that take a value, offered as rows that put an example in the search bar. export const prompts: { section: string; title: string; - icon: Icon; + icon: IconName; example: string; + // The command in [keys] syntax, with a placeholder for the value. + syntax: string; hint: string; keywords: string[]; }[] = [ { section: "Workspaces", title: "Go to Workspace…", - icon: Icon.ArrowRight, + icon: "ArrowRight", example: "1", + syntax: "workspace ", hint: "Type a workspace's number or the start of its name, such as 2 or web.", keywords: ["workspace", "go", "switch", "number", "name"], }, { section: "Widths and Stacks", title: "Set Column Width…", - icon: Icon.ArrowsExpand, + icon: "ArrowsExpand", example: "60", + syntax: "set-column-width ", hint: "Type a width: 60 for 60%, +10 or -10 to change it, or 960 for points.", keywords: ["width", "resize", "size", "column"], }, { section: "Widths and Stacks", title: "Set Window Height…", - icon: Icon.ChevronUpDown, + icon: "ChevronUpDown", example: "h50", + syntax: "set-window-height ", hint: "Type h and a height for the focused stacked window: h50, h+10, or h-10.", keywords: ["height", "stack", "resize", "size"], }, @@ -86,43 +93,43 @@ export const sections: [string, Entry[]][] = [ [ "Focus", [ - { command: "focus-column left", title: "Focus Left Column", icon: Icon.ArrowLeft, shortcut: "mod-left" }, - { command: "focus-column right", title: "Focus Right Column", icon: Icon.ArrowRight, shortcut: "mod-right" }, - { command: "focus-column first", title: "Focus First Column", icon: Icon.ArrowLeftCircle, shortcut: "mod-home" }, - { command: "focus-column last", title: "Focus Last Column", icon: Icon.ArrowRightCircle, shortcut: "mod-end" }, - { command: "focus-window up", title: "Focus Window Above", icon: Icon.ArrowUp, shortcut: "mod-up" }, - { command: "focus-window down", title: "Focus Window Below", icon: Icon.ArrowDown, shortcut: "mod-down" }, - { command: "focus-window first", title: "Focus Top Window", icon: Icon.ArrowUpCircle }, - { command: "focus-window last", title: "Focus Bottom Window", icon: Icon.ArrowDownCircle }, - { command: "focus-window-previous", title: "Focus Previous Window", icon: Icon.Undo }, - { command: "focus-window-or-workspace up", title: "Focus Window or Workspace Above", icon: Icon.ArrowUp }, - { command: "focus-window-or-workspace down", title: "Focus Window or Workspace Below", icon: Icon.ArrowDown }, - { command: "focus-column-or-monitor left", title: "Focus Left Column or Left Display", icon: Icon.ArrowLeft }, - { command: "focus-column-or-monitor right", title: "Focus Right Column or Right Display", icon: Icon.ArrowRight }, + { command: "focus-column left", title: "Focus Left Column", icon: "ArrowLeft", shortcut: "mod-left" }, + { command: "focus-column right", title: "Focus Right Column", icon: "ArrowRight", shortcut: "mod-right" }, + { command: "focus-column first", title: "Focus First Column", icon: "ArrowLeftCircle", shortcut: "mod-home" }, + { command: "focus-column last", title: "Focus Last Column", icon: "ArrowRightCircle", shortcut: "mod-end" }, + { command: "focus-window up", title: "Focus Window Above", icon: "ArrowUp", shortcut: "mod-up" }, + { command: "focus-window down", title: "Focus Window Below", icon: "ArrowDown", shortcut: "mod-down" }, + { command: "focus-window first", title: "Focus Top Window", icon: "ArrowUpCircle" }, + { command: "focus-window last", title: "Focus Bottom Window", icon: "ArrowDownCircle" }, + { command: "focus-window-previous", title: "Focus Previous Window", icon: "Undo" }, + { command: "focus-window-or-workspace up", title: "Focus Window or Workspace Above", icon: "ArrowUp" }, + { command: "focus-window-or-workspace down", title: "Focus Window or Workspace Below", icon: "ArrowDown" }, + { command: "focus-column-or-monitor left", title: "Focus Left Column or Left Display", icon: "ArrowLeft" }, + { command: "focus-column-or-monitor right", title: "Focus Right Column or Right Display", icon: "ArrowRight" }, { command: "focus-floating previous", title: "Focus Previous Floating Window", - icon: Icon.ChevronLeft, + icon: "ChevronLeft", shortcut: "mod-shift-comma", }, { command: "focus-floating next", title: "Focus Next Floating Window", - icon: Icon.ChevronRight, + icon: "ChevronRight", shortcut: "mod-shift-period", }, - { command: "focus-floating first", title: "Focus First Floating Window", icon: Icon.ArrowLeftCircle }, - { command: "focus-floating last", title: "Focus Last Floating Window", icon: Icon.ArrowRightCircle }, + { command: "focus-floating first", title: "Focus First Floating Window", icon: "ArrowLeftCircle" }, + { command: "focus-floating last", title: "Focus Last Floating Window", icon: "ArrowRightCircle" }, { command: "activate-floating", title: "Bring Floating Windows Forward", - icon: Icon.Layers, + icon: "Layers", shortcut: "mod-shift-v", }, - ...(["left", "right", "up", "down"] as const).map((direction) => ({ + ...(["left", "right", "up", "down"] as const).map((direction): Entry => ({ command: `focus-monitor ${direction}`, title: `Focus ${displayName(direction)}`, - icon: Icon.Monitor, + icon: "Monitor", shortcut: `mod-shift-${direction}`, })), ], @@ -130,135 +137,135 @@ export const sections: [string, Entry[]][] = [ [ "Workspaces", [ - { command: "focus-workspace up", title: "Go to Workspace Above", icon: Icon.ChevronUp, shortcut: "mod-i" }, - { command: "focus-workspace down", title: "Go to Workspace Below", icon: Icon.ChevronDown, shortcut: "mod-u" }, - { command: "focus-workspace-previous", title: "Go to Previous Workspace", icon: Icon.Undo }, - { command: "reorder-workspace up", title: "Move Workspace Up", icon: Icon.ArrowUp, shortcut: "mod-shift-i" }, + { command: "focus-workspace up", title: "Go to Workspace Above", icon: "ChevronUp", shortcut: "mod-i" }, + { command: "focus-workspace down", title: "Go to Workspace Below", icon: "ChevronDown", shortcut: "mod-u" }, + { command: "focus-workspace-previous", title: "Go to Previous Workspace", icon: "Undo" }, + { command: "reorder-workspace up", title: "Move Workspace Up", icon: "ArrowUp", shortcut: "mod-shift-i" }, { command: "reorder-workspace down", title: "Move Workspace Down", - icon: Icon.ArrowDown, + icon: "ArrowDown", shortcut: "mod-shift-u", }, - ...(["left", "right", "up", "down"] as const).map((direction) => ({ + ...(["left", "right", "up", "down"] as const).map((direction): Entry => ({ command: `move-workspace-to-monitor ${direction}`, title: `Move Workspace to ${displayName(direction)}`, - icon: Icon.Monitor, + icon: "Monitor", })), ], ], [ "Move", [ - { command: "move-column left", title: "Move Column Left", icon: Icon.ArrowLeft, shortcut: "mod-cmd-left" }, - { command: "move-column right", title: "Move Column Right", icon: Icon.ArrowRight, shortcut: "mod-cmd-right" }, + { command: "move-column left", title: "Move Column Left", icon: "ArrowLeft", shortcut: "mod-cmd-left" }, + { command: "move-column right", title: "Move Column Right", icon: "ArrowRight", shortcut: "mod-cmd-right" }, { command: "move-column first", title: "Move Column to Start", - icon: Icon.ArrowLeftCircle, + icon: "ArrowLeftCircle", shortcut: "mod-cmd-home", }, { command: "move-column last", title: "Move Column to End", - icon: Icon.ArrowRightCircle, + icon: "ArrowRightCircle", shortcut: "mod-cmd-end", }, - { command: "move-window up", title: "Move Window Up", icon: Icon.ArrowUp, shortcut: "mod-cmd-up" }, - { command: "move-window down", title: "Move Window Down", icon: Icon.ArrowDown, shortcut: "mod-cmd-down" }, + { command: "move-window up", title: "Move Window Up", icon: "ArrowUp", shortcut: "mod-cmd-up" }, + { command: "move-window down", title: "Move Window Down", icon: "ArrowDown", shortcut: "mod-cmd-down" }, { command: "move-column-to-workspace up", title: "Move Column to Workspace Above", - icon: Icon.ChevronUp, + icon: "ChevronUp", shortcut: "mod-cmd-i", }, { command: "move-column-to-workspace down", title: "Move Column to Workspace Below", - icon: Icon.ChevronDown, + icon: "ChevronDown", shortcut: "mod-cmd-u", }, - { command: "move-window-to-workspace up", title: "Move Window to Workspace Above", icon: Icon.ChevronUp }, - { command: "move-window-to-workspace down", title: "Move Window to Workspace Below", icon: Icon.ChevronDown }, - { command: "send-column-to-workspace up", title: "Send Column to Workspace Above", icon: Icon.ArrowNe }, - { command: "send-column-to-workspace down", title: "Send Column to Workspace Below", icon: Icon.ArrowNe }, - { command: "send-window-to-workspace up", title: "Send Window to Workspace Above", icon: Icon.ArrowNe }, - { command: "send-window-to-workspace down", title: "Send Window to Workspace Below", icon: Icon.ArrowNe }, - ...(["left", "right", "up", "down"] as const).map((direction) => ({ + { command: "move-window-to-workspace up", title: "Move Window to Workspace Above", icon: "ChevronUp" }, + { command: "move-window-to-workspace down", title: "Move Window to Workspace Below", icon: "ChevronDown" }, + { command: "send-column-to-workspace up", title: "Send Column to Workspace Above", icon: "ArrowNe" }, + { command: "send-column-to-workspace down", title: "Send Column to Workspace Below", icon: "ArrowNe" }, + { command: "send-window-to-workspace up", title: "Send Window to Workspace Above", icon: "ArrowNe" }, + { command: "send-window-to-workspace down", title: "Send Window to Workspace Below", icon: "ArrowNe" }, + ...(["left", "right", "up", "down"] as const).map((direction): Entry => ({ command: `move-column-to-monitor ${direction}`, title: `Move Column to ${displayName(direction)}`, - icon: Icon.Monitor, + icon: "Monitor", shortcut: `mod-shift-cmd-${direction}`, })), - { command: "move-column-or-to-monitor left", title: "Move Column Left or to Left Display", icon: Icon.ArrowLeft }, + { command: "move-column-or-to-monitor left", title: "Move Column Left or to Left Display", icon: "ArrowLeft" }, { command: "move-column-or-to-monitor right", title: "Move Column Right or to Right Display", - icon: Icon.ArrowRight, + icon: "ArrowRight", }, - ...(["left", "right", "up", "down"] as const).map((direction) => ({ + ...(["left", "right", "up", "down"] as const).map((direction): Entry => ({ command: `move-window-to-monitor ${direction}`, title: `Move Window to ${displayName(direction)}`, - icon: Icon.Monitor, + icon: "Monitor", })), ], ], [ "Widths and Stacks", [ - { command: "cycle-width next", title: "Next Column Width", icon: Icon.ArrowsExpand, shortcut: "mod-equal" }, + { command: "cycle-width next", title: "Next Column Width", icon: "ArrowsExpand", shortcut: "mod-equal" }, { command: "cycle-width previous", title: "Previous Column Width", - icon: Icon.ArrowsContract, + icon: "ArrowsContract", shortcut: "mod-minus", }, - { command: "maximize-column", title: "Maximize Column", icon: Icon.Maximize, shortcut: "mod-f" }, - { command: "expand-column-to-available-width", title: "Fill Available Width", icon: Icon.ArrowsExpand }, - { command: "center-column", title: "Center Column", icon: Icon.Center, shortcut: "mod-c" }, - { command: "center-visible-columns", title: "Center Visible Columns", icon: Icon.AlignCentre }, - { command: "switch-preset-window-height", title: "Next Window Height", icon: Icon.ChevronUpDown }, - { command: "reset-window-height", title: "Reset Window Heights", icon: Icon.RotateAntiClockwise }, + { command: "maximize-column", title: "Maximize Column", icon: "Maximize", shortcut: "mod-f" }, + { command: "expand-column-to-available-width", title: "Fill Available Width", icon: "ArrowsExpand" }, + { command: "center-column", title: "Center Column", icon: "Center", shortcut: "mod-c" }, + { command: "center-visible-columns", title: "Center Visible Columns", icon: "AlignCentre" }, + { command: "switch-preset-window-height", title: "Next Window Height", icon: "ChevronUpDown" }, + { command: "reset-window-height", title: "Reset Window Heights", icon: "RotateAntiClockwise" }, { command: "join-window left", title: "Stack into Left Column", - icon: Icon.AppWindowList, + icon: "AppWindowList", shortcut: "mod-semicolon", }, { command: "join-window right", title: "Stack into Right Column", - icon: Icon.AppWindowList, + icon: "AppWindowList", shortcut: "mod-quote", }, - { command: "unjoin-windows", title: "Unstack Window", icon: Icon.AppWindowGrid2x2, shortcut: "mod-period" }, + { command: "unjoin-windows", title: "Unstack Window", icon: "AppWindowGrid2x2", shortcut: "mod-period" }, { command: "consume-or-expel-window left", title: "Stack or Unstack Left", - icon: Icon.AppWindowSidebarLeft, + icon: "AppWindowSidebarLeft", shortcut: "mod-leftbracket", }, { command: "consume-or-expel-window right", title: "Stack or Unstack Right", - icon: Icon.AppWindowSidebarRight, + icon: "AppWindowSidebarRight", shortcut: "mod-rightbracket", }, - { command: "swap-window left", title: "Swap with Left Window", icon: Icon.Switch }, - { command: "swap-window right", title: "Swap with Right Window", icon: Icon.Switch }, - { command: "toggle-floating", title: "Float or Tile Window", icon: Icon.Window, shortcut: "mod-v" }, + { command: "swap-window left", title: "Swap with Left Window", icon: "Switch" }, + { command: "swap-window right", title: "Swap with Right Window", icon: "Switch" }, + { command: "toggle-floating", title: "Float or Tile Window", icon: "Window", shortcut: "mod-v" }, ], ], [ "Other", [ - { command: "toggle-overview", title: "Toggle Overview", icon: Icon.AppWindowGrid3x3, shortcut: "mod-o" }, - { command: "toggle-cheatsheet", title: "Toggle Cheatsheet", icon: Icon.Keyboard, shortcut: "mod-shift-slash" }, - { command: "open-settings", title: "Open Settings", icon: Icon.Gear }, + { command: "toggle-overview", title: "Toggle Overview", icon: "AppWindowGrid3x3", shortcut: "mod-o" }, + { command: "toggle-cheatsheet", title: "Toggle Cheatsheet", icon: "Keyboard", shortcut: "mod-shift-slash" }, + { command: "open-settings", title: "Open Settings", icon: "Gear" }, { command: "diagnostic-mark", title: "Mark a Problem for Diagnostics", - icon: Icon.Bug, + icon: "Bug", hud: "Marked for diagnostics", }, ], diff --git a/contrib/raycast/src/defi.ts b/contrib/raycast/src/defi.ts index 3be69ea6..9fcb0298 100644 --- a/contrib/raycast/src/defi.ts +++ b/contrib/raycast/src/defi.ts @@ -56,7 +56,8 @@ export async function runAndClose(commands: string[][], hud?: string): Promise (desktop.isLoading || !desktop.data ? await readDesktop() : desktop.data), displayCount: new Set(desktop.data?.workspaces.map((workspace) => workspace.display)).size, focused: windows?.find((window) => window.focused), // The workspace the user is on, even when it holds no window. diff --git a/contrib/raycast/src/error-view.tsx b/contrib/raycast/src/error-view.tsx index c5633456..e9c4b5d3 100644 --- a/contrib/raycast/src/error-view.tsx +++ b/contrib/raycast/src/error-view.tsx @@ -1,5 +1,5 @@ -import { Action, ActionPanel, Icon, List, openExtensionPreferences } from "@raycast/api"; -import { defiAppPath, notInstalled, notRunning } from "./defi"; +import { Action, ActionPanel, Icon, List, open, openExtensionPreferences, showToast, Toast } from "@raycast/api"; +import { defi, defiAppPath, notInstalled, notRunning } from "./defi"; import { VersionMismatch } from "./parse"; const releases = "https://github.com/qeude/Defi/releases/latest"; @@ -28,6 +28,28 @@ export function describeError(error: Error) { return { icon: Icon.Warning, title: "Cannot Reach Defi", description: error.message }; } +const launchAttempts = 10; + +// Opens Defi and keeps the launcher open, retrying until Defi answers. +async function launch(onRetry: () => void) { + const toast = await showToast({ style: Toast.Style.Animated, title: "Launching Defi…" }); + await open(defiAppPath()); + for (let attempt = 0; attempt < launchAttempts; attempt += 1) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + try { + await defi("list-workspaces", "--json"); + onRetry(); + toast.style = Toast.Style.Success; + toast.title = "Defi Is Running"; + return; + } catch { + // Not answering yet. + } + } + toast.style = Toast.Style.Failure; + toast.title = "Defi Did Not Start"; +} + // Recovery for every read failure: install or launch Defi, try again, or pick the app in the preferences. export function RecoveryActions({ error, onRetry }: { error: Error; onRetry: () => void }) { const preferences = ( @@ -44,12 +66,7 @@ export function RecoveryActions({ error, onRetry }: { error: Error; onRetry: () return ( {error.message === notRunning ? ( - [1000, 2500, 5000].forEach((delay) => setTimeout(onRetry, delay))} - /> + launch(onRetry)} /> ) : null} {preferences} diff --git a/contrib/raycast/src/run-command.tsx b/contrib/raycast/src/run-command.tsx index 5e840833..913e3026 100644 --- a/contrib/raycast/src/run-command.tsx +++ b/contrib/raycast/src/run-command.tsx @@ -31,7 +31,7 @@ function CommandDetail({ text, command, shortcut }: { text: string; command?: st {shortcut !== undefined ? ( ) : null} @@ -70,7 +70,7 @@ export default function Command(props: LaunchProps<{ arguments: Arguments.RunCom const item = (entry: (typeof entries)[number], keyPrefix = "") => ( runAndClose([suggestion.args], suggestion.hud)} /> - {suggestion.binding ? ( - - ) : null} + {/* Copy keeps ⌘↵ on every row; without a [keys] form, it copies the CLI call. */} + {toggleDetail} } @@ -160,7 +162,7 @@ export default function Command(props: LaunchProps<{ arguments: Arguments.RunCom .map((prompt) => ( } @@ -171,6 +173,7 @@ export default function Command(props: LaunchProps<{ arguments: Arguments.RunCom icon={Icon.TextCursor} onAction={() => setSearchText(prompt.example)} /> + {toggleDetail} } diff --git a/contrib/raycast/src/search-workspaces.tsx b/contrib/raycast/src/search-workspaces.tsx index b5a9905a..7d50461b 100644 --- a/contrib/raycast/src/search-workspaces.tsx +++ b/contrib/raycast/src/search-workspaces.tsx @@ -31,14 +31,17 @@ export default function Command(props: LaunchProps) { // Cached rows would look live while Defi is unreachable, so an error hides them. const shown = desktop.error ? [] : (workspaces ?? []); const displays = [...new Set(shown.map((workspace) => workspace.display))]; - const moving = focused?.appName ?? "window"; const actions = (workspace: DefiWorkspace, label: string) => { - const elsewhere = focused !== undefined && focused.workspace?.id !== workspace.id; - const move = (command: string, hud: string) => () => - elsewhere - ? runAndClose([[command, workspace.id]], hud) - : showToast({ style: Toast.Style.Failure, title: focused ? `Already on ${label}` : "No Focused Window" }); + // The cached focus may be stale while the list reloads, so moves check a fresh read. + const move = (command: string, describe: (app: string) => string) => async () => { + const latest = (await desktop.fresh()).windows.find((window) => window.focused); + if (!latest || latest.workspace?.id === workspace.id) { + await showToast({ style: Toast.Style.Failure, title: latest ? `Already on ${label}` : "No Focused Window" }); + return; + } + await runAndClose([[command, workspace.id]], describe(latest.appName)); + }; return ( @@ -51,25 +54,25 @@ export default function Command(props: LaunchProps) { `Moved ${app} to ${label}`)} /> `Sent ${app} to ${label}`)} /> `Moved column to ${label}`)} /> `Sent column to ${label}`)} /> @@ -127,18 +130,21 @@ export default function Command(props: LaunchProps) { tooltip: appName(app), })); const extra = icons.length - maxAppIcons; - const activeTag = workspace.active ? [{ tag: { value: "Active", color: Color.Blue } }] : []; return ( 0 ? [{ text: `+${extra}` }] : []), ...activeTag] + ? undefined + : [ + ...icons.slice(0, maxAppIcons), + ...(extra > 0 ? [{ text: `+${extra}` }] : []), + ...(workspace.active ? [{ tag: { value: "Active", color: Color.Blue } }] : []), + ] } detail={ + {workspace.active ? ( + + + + ) : null} {displayCount > 1 ? ( ) : null} diff --git a/contrib/raycast/src/suggest.ts b/contrib/raycast/src/suggest.ts index 6a107a71..098d0ff1 100644 --- a/contrib/raycast/src/suggest.ts +++ b/contrib/raycast/src/suggest.ts @@ -13,13 +13,17 @@ export type Suggestion = { }; // Go to, then the four moves in the order Search Workspaces offers them. -function workspaceSuggestions(workspace: DefiWorkspace): Suggestion[] { +function workspaceSuggestions(workspace: DefiWorkspace, namedCount: number): Suggestion[] { const label = workspaceLabel(workspace); const named = workspace.name !== undefined; const target = (byName: string, byPosition?: string) => named ? `${byName} ${workspace.name}` : byPosition && `${byPosition} ${workspace.position}`; - // mod-1 … mod-9 reach the first nine workspaces of the focused display. - const digit = workspace.focusedDisplay && workspace.position <= 9 ? String(workspace.position) : undefined; + // mod-1 … mod-9 go to the named workspaces in [workspaces].names order, which the CLI does not + // report, then to positions on the focused display; only that second case is known here. + const digit = + !named && workspace.focusedDisplay && workspace.position > namedCount && workspace.position <= 9 + ? String(workspace.position) + : undefined; return [ { title: `Go to ${label}`, @@ -105,11 +109,15 @@ function matchesName(name: string, text: string): boolean { return name.toLowerCase().startsWith(lowered) || words.some((word) => word.startsWith(lowered)); } +const minNameQuery = 2; + // Commands whose argument is the search text: "60" sets a width, "h50" a stacked window's height, // "+10" widens, "2" or "web" targets a workspace. export function suggestions(query: string, workspaces: DefiWorkspace[]): Suggestion[] { const text = query.trim(); if (!text) return []; + const namedCount = workspaces.filter((workspace) => workspace.kind === "named").length; + const forWorkspace = (workspace: DefiWorkspace) => workspaceSuggestions(workspace, namedCount); const result: Suggestion[] = []; const width = text.match(/^([+-])?(\d{1,4})\s*(%|pt|px)?$/i); @@ -118,12 +126,13 @@ export function suggestions(query: string, workspaces: DefiWorkspace[]): Suggest const amount = Number(digits); const byPosition = !sign && !unit && workspaces.find((w) => w.focusedDisplay && w.position === amount); const points = unit ? unit !== "%" : amount > 100; - const forWorkspace = byPosition ? workspaceSuggestions(byPosition) : []; - result.push(...forWorkspace.slice(0, 1)); + const [goTo, moveWindow] = byPosition ? forWorkspace(byPosition) : []; + if (goTo) result.push(goTo); // Defi clamps a width to at least 5% of the usable width. - if (sign ? amount > 0 : points || amount >= 5) + if (sign ? amount > 0 : points || amount >= 5) { result.push(sized("width", sign as "+" | "-" | undefined, amount, points)); - result.push(...forWorkspace.slice(1)); + } + if (moveWindow) result.push(moveWindow); } const height = text.match(/^h\s*([+-])?(\d{1,3})\s*%?$/i); @@ -131,8 +140,13 @@ export function suggestions(query: string, workspaces: DefiWorkspace[]): Suggest result.push(sized("height", height[1] as "+" | "-" | undefined, Number(height[2]), false)); } - for (const workspace of workspaces) { - if (workspace.name && matchesName(workspace.name, text)) result.push(...workspaceSuggestions(workspace)); + // A partial name offers the two common actions; the exact name offers all five. + if (text.length >= minNameQuery) { + for (const workspace of workspaces) { + if (!workspace.name || !matchesName(workspace.name, text)) continue; + const all = forWorkspace(workspace); + result.push(...(workspace.name.toLowerCase() === text.toLowerCase() ? all : all.slice(0, 2))); + } } return result; } diff --git a/contrib/raycast/src/window-list.tsx b/contrib/raycast/src/window-list.tsx index 0c4a917b..74d546aa 100644 --- a/contrib/raycast/src/window-list.tsx +++ b/contrib/raycast/src/window-list.tsx @@ -17,11 +17,10 @@ export function WindowList({ initialSearch = "", workspaceID }: { initialSearch? const { windows, workspaces, displayCount, appIcon, current: here } = desktop; const [searchText, setSearchText] = useState(initialSearch); const [showDetail, setShowDetail] = useCachedState("windows-strip", false); - const [storedScope, setScope] = useCachedState("window-scope", ""); + // Every launch starts on all workspaces, so a narrowed list never surprises. + const [chosenScope, setScope] = useState(""); const withWindows = workspaces?.filter((workspace) => workspace.windowCount > 0) ?? []; - // A remembered workspace that has since emptied or gone falls back to all workspaces. - const scope = - workspaceID ?? (storedScope === current || withWindows.some((w) => w.id === storedScope) ? storedScope : ""); + const scope = workspaceID ?? chosenScope; // shortcut: ranks windows by picks made here, not by Defi's focus history, and keeps the ranks of // closed windows; use Defi's order once list-windows exposes it (feat/recent-windows-switcher keeps one). const { data: ranked, visitItem } = useFrecencySorting(windows, { key: (window) => String(window.id) }); @@ -113,7 +112,6 @@ export function WindowList({ initialSearch = "", workspaceID }: { initialSearch? const workspace = window.workspace; const label = workspace ? workspaceLabel(workspace) : undefined; const fullWorkspace = workspaces?.find((candidate) => candidate.id === workspace?.id); - const focusedTag = window.focused ? [{ tag: { value: "Focused", color: Color.Blue } }] : []; return ( 1 ? [`display ${window.display}`] : []), ]} - // The side pane repeats the rest, so rows next to it keep only the focus tag. + // Rows next to the side pane stay bare; the pane carries the same facts. accessories={ showDetail - ? focusedTag + ? undefined : [ - ...focusedTag, + ...(window.focused ? [{ tag: { value: "Focused", color: Color.Blue } }] : []), ...(window.floating ? [{ tag: "Floating" }] : []), ...(fullWorkspace && label ? [ @@ -160,6 +158,14 @@ export function WindowList({ initialSearch = "", workspaceID }: { initialSearch? ) : null} + {window.focused || window.floating ? ( + + {window.focused ? ( + + ) : null} + {window.floating ? : null} + + ) : null} ) : undefined } @@ -193,13 +199,16 @@ export function WindowList({ initialSearch = "", workspaceID }: { initialSearch? title={`Bring to ${workspaceLabel(here)}`} icon={Icon.ArrowDownCircle} shortcut={{ modifiers: ["shift"], key: "return" }} - onAction={() => - act( + onAction={async () => { + // The cached current workspace may be stale while the list reloads. + const latest = (await desktop.fresh()).workspaces.find((w) => w.active && w.focusedDisplay); + const target = latest && latest.id !== workspace.id ? latest : undefined; + await act( window, - [["move-window-to-workspace-name", here.id]], - `Brought ${window.appName} to ${workspaceLabel(here)}`, - ) - } + target ? [["move-window-to-workspace-name", target.id]] : [], + target ? `Brought ${window.appName} to ${workspaceLabel(target)}` : undefined, + ); + }} /> ) : null} line.startsWith("| `")); +const entries = sections.flatMap(([, list]) => list); + +test("every catalog command is a documented command with a description", () => { + for (const { command } of entries) { + const verb = command.split(" ")[0]; + assert.ok( + rows.some((row) => row.startsWith(`| \`${verb} `) || row.startsWith(`| \`${verb}\``)), + `${verb} is not in CONFIGURATION.md`, + ); + assert.ok(descriptions[verb], `${verb} has no description`); + } + assert.ok(prompts.every((prompt) => sections.some(([section]) => section === prompt.section))); +}); + +test("every default shortcut matches the documented one", () => { + for (const { command, shortcut } of entries.filter((entry) => entry.shortcut)) { + const [verb, direction = ""] = command.split(" "); + // A command can have one row per group of arguments, such as left|right and first|last. + const row = + rows.find((line) => { + const cell = line.split("`")[1] ?? ""; + return cell.split(" ")[0] === verb && (!direction || cell.includes(direction)); + }) ?? ""; + // The reference writes the four directions of one binding as . + const general = shortcut!.replace(new RegExp(`-${direction}$`), "-"); + assert.ok( + row.includes(`\`${shortcut}\``) || row.includes(`\`${general}\``), + `${command}: ${shortcut} is not its documented shortcut`, + ); + } +}); diff --git a/contrib/raycast/test/view.test.ts b/contrib/raycast/test/view.test.ts index df9061f0..74b78cbc 100644 --- a/contrib/raycast/test/view.test.ts +++ b/contrib/raycast/test/view.test.ts @@ -37,50 +37,40 @@ test("search text becomes a command argument", () => { assert.deepEqual(titles("3"), [ ["Go to dev-secondary", "--monitor 1 workspace dev-secondary"], ["Move Window to dev-secondary", "move-window-to-workspace-name dev-secondary"], - ["Send Window to dev-secondary", "send-window-to-workspace-name dev-secondary"], - ["Move Column to dev-secondary", "move-column-to-workspace-name dev-secondary"], - ["Send Column to dev-secondary", "send-column-to-workspace-name dev-secondary"], ]); - assert.deepEqual(titles("sec").map(([title]) => title)[0], "Go to dev-secondary"); assert.deepEqual(titles("7"), [["Set Column Width to 7%", "set-column-width 7%"]]); assert.deepEqual(titles("+10"), [["Widen Column by 10%", "set-column-width +10%"]]); assert.deepEqual(titles("-40pt"), [["Narrow Column by 40 pt", "set-column-width -40"]]); assert.deepEqual(titles("960"), [["Set Column Width to 960 pt", "set-column-width 960"]]); assert.deepEqual(titles("h50"), [["Set Window Height to 50%", "set-window-height 50%"]]); assert.deepEqual(titles("h-10"), [["Shrink Window by 10%", "set-window-height -10%"]]); - assert.deepEqual(titles("We"), [ + // A partial name offers the two common actions, a word inside the name counts, one letter offers none. + assert.deepEqual(titles("we"), [ ["Go to web", "--monitor 1 workspace web"], ["Move Window to web", "move-window-to-workspace-name web"], - ["Send Window to web", "send-window-to-workspace-name web"], - ["Move Column to web", "move-column-to-workspace-name web"], - ["Send Column to web", "send-column-to-workspace-name web"], ]); + assert.equal(titles("sec")[0][0], "Go to dev-secondary"); + assert.deepEqual(titles("w"), []); + assert.deepEqual( + titles("web").map(([title]) => title), + ["Go to web", "Move Window to web", "Send Window to web", "Move Column to web", "Send Column to web"], + ); assert.deepEqual(titles("zzz"), []); assert.deepEqual(titles(" "), []); }); -test("copied commands use the form [keys] accepts", () => { +test("copied commands use the form [keys] accepts, and digit shortcuts only where Defi binds them", () => { const bindings = (query: string) => suggestions(query, workspaces).map((s) => s.binding); assert.deepEqual(bindings("web").slice(0, 2), ["workspace web", "move-window-to-workspace-name web"]); - assert.deepEqual(bindings("5").slice(0, 2), ["focus-workspace-position 5", "set-column-width 5%"]); - const unnamed = { ...workspaces[4], kind: "ordinary" }; - assert.deepEqual( - suggestions("5", [unnamed]).map((s) => s.binding), - [ - "focus-workspace-position 5", - "set-column-width 5%", - "move-window-to-workspace-position 5", - "send-window-to-workspace-position 5", - "move-column-to-workspace-position 5", - undefined, - ], - ); - assert.deepEqual( - suggestions("2", workspaces) - .map((s) => s.shortcut) - .filter(Boolean), - ["mod-2", "mod-cmd-2"], - ); + assert.deepEqual(bindings("5"), [ + "focus-workspace-position 5", + "set-column-width 5%", + "move-window-to-workspace-position 5", + ]); + // Named workspaces take mod-1 … mod-4 in an order the CLI does not report; position 5 follows them. + const shortcuts = (query: string) => suggestions(query, workspaces).map((s) => s.shortcut); + assert.deepEqual(shortcuts("2"), [undefined, undefined]); + assert.deepEqual(shortcuts("5"), ["mod-5", undefined, undefined]); assert.deepEqual( suggestions("+10", workspaces).map((s) => s.hud), ["Widened column by 10%"], From 9fa1851f53f80fce1868b00b46b201fc4d0217ef Mon Sep 17 00:00:00 2001 From: Quentin Eude Date: Wed, 11 Nov 2026 15:12:03 +0100 Subject: [PATCH 11/23] fix(contrib): move windows through a pushed list and describe every row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tinycast flattens submenus, so ⌘↵ on Move to Workspace would have moved the window to the first workspace; push a list grouped by display instead. Describe each Search Commands row in its own words, name the window in its side pane, report read failures where they are seen, link releases for an old Defi, and add Refresh to Search Commands. --- contrib/raycast/README.md | 2 +- contrib/raycast/src/commands.ts | 93 +++++++++++++------ contrib/raycast/src/error-view.tsx | 13 ++- contrib/raycast/src/run-command.tsx | 25 +++-- contrib/raycast/src/search-workspaces.tsx | 17 +++- contrib/raycast/src/window-list.tsx | 108 +++++++++++++++------- contrib/raycast/test/commands.test.ts | 7 +- 7 files changed, 192 insertions(+), 73 deletions(-) diff --git a/contrib/raycast/README.md b/contrib/raycast/README.md index 2ac80c9b..0b11f5dd 100644 --- a/contrib/raycast/README.md +++ b/contrib/raycast/README.md @@ -10,7 +10,7 @@ titles stay on your Mac. - **Search Windows** finds any window Defi manages by title, app, or workspace, and the dropdown narrows it to one workspace. Enter focuses the - window, ⌘↵ moves it to another workspace, ⇧↵ brings it to the current one, + window, ⌘↵ picks a workspace to move it to, ⇧↵ brings it to the current one, and ⌘⇧F floats or tiles it. Windows you pick often come first; the focused one comes last. Acting on a window from another workspace shows that workspace for a moment, because Defi focuses the window first. diff --git a/contrib/raycast/src/commands.ts b/contrib/raycast/src/commands.ts index c89f58ee..63a2ac7c 100644 --- a/contrib/raycast/src/commands.ts +++ b/contrib/raycast/src/commands.ts @@ -44,43 +44,82 @@ export const prompts: { }, ]; -// What each command does, from CONFIGURATION.md "Commands". -export const descriptions: Record = { - "focus-column": "Focus the previous, next, first, or last column.", - "focus-window": "Focus another window in the focused column's stack.", +const place: Record = { left: "on the left", right: "on the right", up: "above", down: "below" }; +const end: Record = { first: "start", last: "end" }; + +// What one row does, worded for its own argument; from CONFIGURATION.md "Commands". +export function describeCommand(command: string): string { + const [verb, arg = ""] = command.split(" "); + const where = place[arg]; + const display = `the display ${where}`; + const workspace = `the workspace ${where}`; + switch (verb) { + case "focus-column": + return where ? `Focus the column ${where}.` : `Focus the ${arg} column of the strip.`; + case "focus-window": + return where + ? `Focus the window ${where} in the stack.` + : `Focus the ${arg === "first" ? "top" : "bottom"} window of the stack.`; + case "focus-window-or-workspace": + return `Focus the window ${where}, or past the ${arg === "up" ? "top" : "bottom"} of the stack, ${workspace}.`; + case "focus-column-or-monitor": + return `Focus the column ${where}, or past the ${arg === "left" ? "first" : "last"} column, ${display}.`; + case "focus-floating": + return `Focus the ${arg} floating window${arg === "previous" || arg === "next" ? ", wrapping around" : ""}.`; + case "focus-monitor": + return `Focus ${display}.`; + case "focus-workspace": + return `Go to ${workspace} on this display, without wrapping.`; + case "reorder-workspace": + return `Move the active workspace ${arg} its display's stack.`; + case "move-workspace-to-monitor": + return `Move the active workspace to ${display} and remember it there.`; + case "move-column": + return where + ? `Move the focused column one place ${where}.` + : `Move the focused column to the ${end[arg]} of the strip.`; + case "move-window": + return `Move the focused window ${arg} its stack.`; + case "move-column-to-workspace": + return `Move the focused column to ${workspace} and follow it.`; + case "move-window-to-workspace": + return `Move only the focused window to ${workspace} and follow it.`; + case "send-column-to-workspace": + return `Move the focused column to ${workspace} without following it.`; + case "send-window-to-workspace": + return `Move only the focused window to ${workspace} without following it.`; + case "move-column-to-monitor": + return `Move the focused column to ${display}.`; + case "move-column-or-to-monitor": + return `Move the focused column ${arg}, or past the ${arg === "left" ? "first" : "last"} column, to ${display}.`; + case "move-window-to-monitor": + return `Move only the focused window to ${display}.`; + case "cycle-width": + return `Step to the ${arg} preset column width, wrapping around.`; + case "join-window": + return `Add the focused window to the stack ${where}.`; + case "consume-or-expel-window": + return `Alone in its column, the window joins the stack ${where}; in a stack, it leaves as a new column ${where}.`; + case "swap-window": + return `Swap the focused window with the one at the same row of the column ${where}.`; + default: + return descriptions[verb] ?? ""; + } +} + +// Commands that take no argument, or whose argument changes nothing in the wording. +const descriptions: Record = { "focus-window-previous": "Go back to the window focused before this one, on any workspace or display.", - "focus-window-or-workspace": - "Like Focus Window, but past the top or bottom of the stack, focus the adjacent workspace.", - "focus-column-or-monitor": "Like Focus Column, but past the first or last column, focus the next display.", - "focus-floating": "Focus another floating window, wrapping around.", "activate-floating": "Select the floating layer and bring its selected window forward.", - "focus-monitor": "Focus the nearest display in that direction.", - "focus-workspace": "Focus the adjacent workspace on this display, without wrapping.", "focus-workspace-previous": "Go back to the workspace this display showed before.", - "reorder-workspace": "Move the active workspace up or down its display's stack.", - "move-workspace-to-monitor": "Move the active workspace to another display and remember it there.", - "move-column": "Move the focused column within the strip.", - "move-window": "Move the focused window within its stack.", - "move-column-to-workspace": "Move the focused column to the adjacent workspace and follow it.", - "move-window-to-workspace": "Move only the focused window to the adjacent workspace and follow it.", - "send-column-to-workspace": "Move the focused column to the adjacent workspace without following it.", - "send-window-to-workspace": "Move only the focused window to the adjacent workspace without following it.", - "move-column-to-monitor": "Move the focused column to the nearest display in that direction.", - "move-column-or-to-monitor": "Like Move Column, but past the first or last column, move it to the next display.", - "move-window-to-monitor": "Move only the focused window to the nearest display in that direction.", - "cycle-width": "Step through the preset column widths, wrapping.", "maximize-column": "Toggle the focused column between full width and its previous width.", "expand-column-to-available-width": "Grow the focused column into the free space beside the other fully visible columns.", "center-column": "Center the focused column until you select another one.", "center-visible-columns": "Center the fully visible columns as one group.", - "switch-preset-window-height": "Give the focused stacked window the next preset height, wrapping.", + "switch-preset-window-height": "Give the focused stacked window the next preset height, wrapping around.", "reset-window-height": "Split the focused stack evenly again.", - "join-window": "Add the focused window to the stack on the left or right.", "unjoin-windows": "Split the focused window out of its stack into a new column.", - "consume-or-expel-window": - "Alone in its column, the window joins the stack on that side; in a stack, it leaves as a new column.", - "swap-window": "Swap the focused window with the one at the same row of the adjacent column.", "toggle-floating": "Switch the focused window between tiled and floating.", "toggle-overview": "Open or close the Overview of every workspace.", "toggle-cheatsheet": "Open or close the shortcut help.", diff --git a/contrib/raycast/src/error-view.tsx b/contrib/raycast/src/error-view.tsx index e9c4b5d3..6a5f2b94 100644 --- a/contrib/raycast/src/error-view.tsx +++ b/contrib/raycast/src/error-view.tsx @@ -33,7 +33,14 @@ const launchAttempts = 10; // Opens Defi and keeps the launcher open, retrying until Defi answers. async function launch(onRetry: () => void) { const toast = await showToast({ style: Toast.Style.Animated, title: "Launching Defi…" }); - await open(defiAppPath()); + try { + await open(defiAppPath()); + } catch (error) { + toast.style = Toast.Style.Failure; + toast.title = "Cannot Launch Defi"; + toast.message = error instanceof Error ? error.message : String(error); + return; + } for (let attempt = 0; attempt < launchAttempts; attempt += 1) { await new Promise((resolve) => setTimeout(resolve, 1000)); try { @@ -48,6 +55,7 @@ async function launch(onRetry: () => void) { } toast.style = Toast.Style.Failure; toast.title = "Defi Did Not Start"; + toast.primaryAction = { title: "Try Again", onAction: () => launch(onRetry) }; } // Recovery for every read failure: install or launch Defi, try again, or pick the app in the preferences. @@ -68,6 +76,9 @@ export function RecoveryActions({ error, onRetry }: { error: Error; onRetry: () {error.message === notRunning ? ( launch(onRetry)} /> ) : null} + {error instanceof VersionMismatch && error.received < error.supported ? ( + + ) : null} {preferences} diff --git a/contrib/raycast/src/run-command.tsx b/contrib/raycast/src/run-command.tsx index 913e3026..41fa6061 100644 --- a/contrib/raycast/src/run-command.tsx +++ b/contrib/raycast/src/run-command.tsx @@ -1,7 +1,7 @@ import { Action, ActionPanel, Icon, Keyboard, LaunchProps, List } from "@raycast/api"; import { useCachedState } from "@raycast/utils"; import { useState } from "react"; -import { descriptions, prompts, sections } from "./commands"; +import { describeCommand, prompts, sections } from "./commands"; import { runAndClose } from "./defi"; import { useDesktop } from "./desktop"; import { describeError, RecoveryActions } from "./error-view"; @@ -59,12 +59,20 @@ export default function Command(props: LaunchProps<{ arguments: Arguments.RunCom }; const toggleDetail = ( - setShowDetail(!showDetail)} - /> + + setShowDetail(!showDetail)} + /> + + ); const item = (entry: (typeof entries)[number], keyPrefix = "") => ( @@ -80,7 +88,7 @@ export default function Command(props: LaunchProps<{ arguments: Arguments.RunCom } detail={ @@ -98,6 +106,7 @@ export default function Command(props: LaunchProps<{ arguments: Arguments.RunCom return ( { // The cached focus may be stale while the list reloads, so moves check a fresh read. const move = (command: string, describe: (app: string) => string) => async () => { - const latest = (await desktop.fresh()).windows.find((window) => window.focused); + let state; + try { + state = await desktop.fresh(); + } catch (error) { + await showFailureToast(error, { title: "Cannot Reach Defi" }); + return; + } + const latest = state.windows.find((window) => window.focused); if (!latest || latest.workspace?.id === workspace.id) { await showToast({ style: Toast.Style.Failure, title: latest ? `Already on ${label}` : "No Focused Window" }); return; @@ -153,7 +160,11 @@ export default function Command(props: LaunchProps) { highlightID={focused?.id} dark={dark} appIcon={appIcon} - empty="No windows yet. Go here, or move the focused window here with ⌘↵." + empty={ + workspace.windowCount + ? "Only floating windows." + : "No windows yet. Go here, or move the focused window here with ⌘↵." + } > (read: () => Promise): Promise { + try { + return await read(); + } catch (error) { + await showFailureToast(error, { title: "Cannot Reach Defi" }); + return undefined; + } +} + +function MoveTargets(props: { + workspaces: DefiWorkspace[]; + displayCount: number; + subject: string; + onMove: (workspace: DefiWorkspace) => void; +}) { + const displays = [...new Set(props.workspaces.map((workspace) => workspace.display))]; + return ( + + {displays.map((display) => ( + 1 ? `Display ${display}` : undefined}> + {props.workspaces + .filter((workspace) => workspace.display === display) + .map((workspace) => ( + + props.onMove(workspace)} /> + + } + /> + ))} + + ))} + + ); +} + // The window switcher; with workspaceID it lists that workspace only, as Search Workspaces opens it. export function WindowList({ initialSearch = "", workspaceID }: { initialSearch?: string; workspaceID?: string }) { const desktop = useDesktop(); @@ -91,7 +132,13 @@ export function WindowList({ initialSearch = "", workspaceID }: { initialSearch? ); }; @@ -131,7 +178,8 @@ export function WindowList({ initialSearch = "", workspaceID }: { initialSearch? : [ ...(window.focused ? [{ tag: { value: "Focused", color: Color.Blue } }] : []), ...(window.floating ? [{ tag: "Floating" }] : []), - ...(fullWorkspace && label + // A list scoped to one workspace would repeat its name on every row. + ...(fullWorkspace && label && !scopeID ? [ { icon: workspaceIcon(fullWorkspace), @@ -152,18 +200,16 @@ export function WindowList({ initialSearch = "", workspaceID }: { initialSearch? appIcon={appIcon} empty="" > + {window.title ? : null} {displayCount > 1 ? ( ) : null} - {window.focused || window.floating ? ( + {window.focused ? ( - {window.focused ? ( - - ) : null} - {window.floating ? : null} + ) : null} @@ -173,27 +219,25 @@ export function WindowList({ initialSearch = "", workspaceID }: { initialSearch? act(window)} /> - - {workspaces - ?.filter((target) => target.id !== workspace?.id) - .map((target) => { - const name = workspaceLabel(target); - return ( - 1 ? `${name} on Display ${target.display}` : name} - onAction={() => - act( - window, - [["move-window-to-workspace-name", target.id]], - `Moved ${window.appName} to ${name}`, - ) - } - /> - ); - })} - + {/* A pushed list, not a submenu: some launchers flatten submenus, which would give ⌘↵ to its first entry. */} + target.id !== workspace?.id)} + displayCount={displayCount} + subject={window.appName} + onMove={(target) => + act( + window, + [["move-window-to-workspace-name", target.id]], + `Moved ${window.appName} to ${workspaceLabel(target)}`, + ) + } + /> + } + /> {here && workspace && here.id !== workspace.id ? ( { // The cached current workspace may be stale while the list reloads. - const latest = (await desktop.fresh()).workspaces.find((w) => w.active && w.focusedDisplay); + const state = await readOrReport(desktop.fresh); + if (!state) return; + const latest = state.workspaces.find((w) => w.active && w.focusedDisplay); const target = latest && latest.id !== workspace.id ? latest : undefined; await act( window, diff --git a/contrib/raycast/test/commands.test.ts b/contrib/raycast/test/commands.test.ts index a5d46598..1d369bcb 100644 --- a/contrib/raycast/test/commands.test.ts +++ b/contrib/raycast/test/commands.test.ts @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { test } from "node:test"; -import { descriptions, prompts, sections } from "../src/commands.ts"; +import { describeCommand, prompts, sections } from "../src/commands.ts"; // The catalog teaches commands and default shortcuts, so it must match the reference it copies. const reference = readFileSync(new URL("../../../CONFIGURATION.md", import.meta.url), "utf8"); @@ -15,9 +15,12 @@ test("every catalog command is a documented command with a description", () => { rows.some((row) => row.startsWith(`| \`${verb} `) || row.startsWith(`| \`${verb}\``)), `${verb} is not in CONFIGURATION.md`, ); - assert.ok(descriptions[verb], `${verb} has no description`); + assert.match(describeCommand(command), /^[A-Z].+\.$/, `${command} has no description`); } assert.ok(prompts.every((prompt) => sections.some(([section]) => section === prompt.section))); + // Each row says what it does itself, not what its whole command family does. + const texts = entries.map((entry) => describeCommand(entry.command)); + assert.equal(new Set(texts).size, texts.length, "two rows share a description"); }); test("every default shortcut matches the documented one", () => { From bfeaeef01e49bb85c3b4f6c95044d8d712af1be9 Mon Sep 17 00:00:00 2001 From: Quentin Eude Date: Wed, 11 Nov 2026 15:18:21 +0100 Subject: [PATCH 12/23] fix(contrib): one vocabulary and fresh move targets in the Raycast extension Read the move targets afresh with the workspace row's app icons, name the focused window the same way in every command, title action sections, remember suggestions in Recent, show loading only when suggestions wait, read unversioned output and timeouts as clear errors, and draw the strip with six-digit colors. --- contrib/raycast/README.md | 4 +- contrib/raycast/src/commands.ts | 6 +- contrib/raycast/src/defi.ts | 1 + contrib/raycast/src/parse.ts | 10 ++- contrib/raycast/src/run-command.tsx | 101 +++++++++++++--------- contrib/raycast/src/search-workspaces.tsx | 29 +++---- contrib/raycast/src/strip.ts | 12 +-- contrib/raycast/src/suggest.ts | 13 +-- contrib/raycast/src/window-list.tsx | 81 +++++++++-------- contrib/raycast/src/workspace-icon.ts | 19 +++- contrib/raycast/test/view.test.ts | 12 ++- 11 files changed, 171 insertions(+), 117 deletions(-) diff --git a/contrib/raycast/README.md b/contrib/raycast/README.md index 0b11f5dd..06161e58 100644 --- a/contrib/raycast/README.md +++ b/contrib/raycast/README.md @@ -41,7 +41,9 @@ cd contrib/raycast npm install && npm run dev ``` -`npm run dev` adds the extension to Raycast in development mode. For Tinycast, +`npm run dev` adds the extension to Raycast in development mode. Give Search +Windows a hotkey or an alias such as `w` in its command settings (⌘K, then +Configure Command) to reach it in one keystroke. For Tinycast, run `npm run tinycast` instead: it builds straight into Tinycast's extensions folder, and Tinycast picks up a new build the next time a command starts. diff --git a/contrib/raycast/src/commands.ts b/contrib/raycast/src/commands.ts index 63a2ac7c..1f5a90b0 100644 --- a/contrib/raycast/src/commands.ts +++ b/contrib/raycast/src/commands.ts @@ -252,10 +252,10 @@ export const sections: [string, Entry[]][] = [ [ "Widths and Stacks", [ - { command: "cycle-width next", title: "Next Column Width", icon: "ArrowsExpand", shortcut: "mod-equal" }, + { command: "cycle-width next", title: "Cycle to Next Column Width", icon: "ArrowsExpand", shortcut: "mod-equal" }, { command: "cycle-width previous", - title: "Previous Column Width", + title: "Cycle to Previous Column Width", icon: "ArrowsContract", shortcut: "mod-minus", }, @@ -263,7 +263,7 @@ export const sections: [string, Entry[]][] = [ { command: "expand-column-to-available-width", title: "Fill Available Width", icon: "ArrowsExpand" }, { command: "center-column", title: "Center Column", icon: "Center", shortcut: "mod-c" }, { command: "center-visible-columns", title: "Center Visible Columns", icon: "AlignCentre" }, - { command: "switch-preset-window-height", title: "Next Window Height", icon: "ChevronUpDown" }, + { command: "switch-preset-window-height", title: "Cycle to Next Window Height", icon: "ChevronUpDown" }, { command: "reset-window-height", title: "Reset Window Heights", icon: "RotateAntiClockwise" }, { command: "join-window left", diff --git a/contrib/raycast/src/defi.ts b/contrib/raycast/src/defi.ts index 9fcb0298..a86dbe3c 100644 --- a/contrib/raycast/src/defi.ts +++ b/contrib/raycast/src/defi.ts @@ -42,6 +42,7 @@ export function defi(...args: string[]): Promise { return new Promise((resolve, reject) => { execFile(cli, args, { timeout: 5000 }, (error, stdout, stderr) => { if (!error) return resolve(stdout); + if (error.killed) return reject(new Error("Defi did not answer in time.")); const message = stderr.trim() || error.message; reject(new Error(message.includes("connect failed") ? notRunning : message)); }); diff --git a/contrib/raycast/src/parse.ts b/contrib/raycast/src/parse.ts index a1bc1d2b..cab0fd42 100644 --- a/contrib/raycast/src/parse.ts +++ b/contrib/raycast/src/parse.ts @@ -38,9 +38,15 @@ export class VersionMismatch extends Error { } // Fields within a version may grow; a new version may break, so it is refused. +// Output that is not versioned JSON comes from a Defi older than the format, so it reads as version 0. function versioned(text: string, version: number): Record { - const json = JSON.parse(text); - if (json?.version !== version) throw new VersionMismatch(Number(json?.version), version); + let json; + try { + json = JSON.parse(text); + } catch { + throw new VersionMismatch(0, version); + } + if (json?.version !== version) throw new VersionMismatch(Number(json?.version) || 0, version); return json; } diff --git a/contrib/raycast/src/run-command.tsx b/contrib/raycast/src/run-command.tsx index 41fa6061..26291bc6 100644 --- a/contrib/raycast/src/run-command.tsx +++ b/contrib/raycast/src/run-command.tsx @@ -11,6 +11,8 @@ import { suggestions, type Suggestion } from "./suggest"; const entries = sections.flatMap(([section, list]) => list.map((entry) => ({ ...entry, section }))); const recentLimit = 5; +type Recent = { command: string } | { suggestion: Suggestion }; + const suggestionIcons: Record = { width: Icon.ArrowsExpand, height: Icon.ChevronUpDown, @@ -44,20 +46,64 @@ function CommandDetail({ text, command, shortcut }: { text: string; command?: st export default function Command(props: LaunchProps<{ arguments: Arguments.RunCommand }>) { // An empty optional argument arrives as "", so fall through to the root-search text. const [searchText, setSearchText] = useState(props.arguments?.query || props.fallbackText || ""); - const [recent, setRecent] = useCachedState("recent-commands", []); + const [recent, setRecent] = useCachedState("recent-runs", []); const [showDetail, setShowDetail] = useCachedState("show-command-detail", true); const desktop = useDesktop(); const problem = desktop.error; const suggested = suggestions(searchText, problem ? [] : (desktop.workspaces ?? [])); - const recentEntries = searchText + const workspaceIDs = new Set(desktop.workspaces?.map((workspace) => workspace.id)); + // A remembered move to a workspace that has since gone is dropped rather than offered. + const recentRows = searchText ? [] - : recent.flatMap((command) => entries.filter((entry) => entry.command === command)); + : recent.filter( + (run) => + "command" in run || + !run.suggestion.args.some((arg) => arg.startsWith("__defi_dynamic_") && !workspaceIDs.has(arg)), + ); + + const remember = (run: Recent) => { + const key = (other: Recent) => ("command" in other ? other.command : other.suggestion.args.join(" ")); + setRecent([run, ...recent.filter((other) => key(other) !== key(run))].slice(0, recentLimit)); + }; const run = (entry: (typeof entries)[number]) => { - setRecent([entry.command, ...recent.filter((other) => other !== entry.command)].slice(0, recentLimit)); + remember({ command: entry.command }); return runAndClose([entry.command.split(" ")], entry.hud); }; + // Runs a suggestion, naming the window it moves from a fresh read. + const runSuggestion = async (suggestion: Suggestion) => { + remember({ suggestion }); + const app = suggestion.hud?.includes("{window}") + ? (await desktop.fresh().catch(() => undefined))?.windows.find((window) => window.focused)?.appName + : undefined; + await runAndClose([suggestion.args], suggestion.hud?.replace("{window}", app ?? "window")); + }; + + const suggestionItem = (suggestion: Suggestion, keyPrefix = "") => ( + + } + actions={ + + runSuggestion(suggestion)} /> + {/* Copy keeps ⌘↵ on every row; without a [keys] form, it copies the CLI call. */} + + {toggleDetail} + + } + /> + ); + const toggleDetail = ( 0)} isShowingDetail={showDetail} searchText={searchText} onSearchTextChange={setSearchText} @@ -124,42 +171,16 @@ export default function Command(props: LaunchProps<{ arguments: Arguments.RunCom ) : null} {suggested.length > 0 ? ( - - {suggested.map((suggestion) => ( - - } - actions={ - - runAndClose([suggestion.args], suggestion.hud)} - /> - {/* Copy keeps ⌘↵ on every row; without a [keys] form, it copies the CLI call. */} - - {toggleDetail} - - } - /> - ))} - + {suggested.map((suggestion) => suggestionItem(suggestion))} ) : null} - {recentEntries.length > 0 ? ( - {recentEntries.map((entry) => item(entry, "recent-"))} + {recentRows.length > 0 ? ( + + {recentRows.map((run) => { + if (!("command" in run)) return suggestionItem(run.suggestion, "recent-"); + const entry = entries.find((candidate) => candidate.command === run.command); + return entry ? item(entry, "recent-") : null; + })} + ) : null} {sections.map(([section]) => ( diff --git a/contrib/raycast/src/search-workspaces.tsx b/contrib/raycast/src/search-workspaces.tsx index af161075..d742362c 100644 --- a/contrib/raycast/src/search-workspaces.tsx +++ b/contrib/raycast/src/search-workspaces.tsx @@ -18,9 +18,7 @@ import { ErrorView } from "./error-view"; import { workspaceLabel, type DefiWorkspace } from "./parse"; import { StripDetail } from "./strip-detail"; import { WindowList } from "./window-list"; -import { workspaceIcon } from "./workspace-icon"; - -const maxAppIcons = 5; +import { workspaceAccessories, workspaceIcon } from "./workspace-icon"; export default function Command(props: LaunchProps) { const desktop = useDesktop(); @@ -55,7 +53,11 @@ export default function Command(props: LaunchProps) { runAndClose([["--monitor", String(workspace.display), "workspace", workspace.id]])} + onAction={() => + workspace.active && workspace.focusedDisplay + ? showToast({ style: Toast.Style.Failure, title: `Already on ${label}` }) + : runAndClose([["--monitor", String(workspace.display), "workspace", workspace.id]]) + } /> {/* The secondary action keeps ⌘↵ on every row, so it stays even where it has nothing to move. */} `Moved ${app} to ${label}`)} /> + + `Sent ${app} to ${label}`)} /> + + {onDisplay.map((workspace) => { const label = workspaceLabel(workspace); - const icons = workspace.applications.map((app) => ({ - icon: appIcon(app) ?? Icon.AppWindow, - tooltip: appName(app), - })); - const extra = icons.length - maxAppIcons; return ( 0 ? [{ text: `+${extra}` }] : []), - ...(workspace.active ? [{ tag: { value: "Active", color: Color.Blue } }] : []), - ] - } + accessories={showDetail ? undefined : workspaceAccessories(workspace, appIcon, appName)} detail={ `; + const [fill, fillOpacity] = highlighted ? [colors.highlight, 1] : colors.window; + const [stroke, strokeOpacity] = highlighted ? [colors.highlight, 1] : colors.stroke; + return ``; }); }); const height = column.inset * 2 + column.height; - const desk = ``; + const desk = ``; return `${desk}${tiles.join("")}`; } diff --git a/contrib/raycast/src/suggest.ts b/contrib/raycast/src/suggest.ts index 098d0ff1..2bfe1217 100644 --- a/contrib/raycast/src/suggest.ts +++ b/contrib/raycast/src/suggest.ts @@ -9,6 +9,7 @@ export type Suggestion = { // Defi's built-in shortcut for it, in [keys] syntax, when it has one. shortcut?: string; kind: "width" | "height" | "workspace" | "column" | "window"; + // `{window}` stands for the focused window's app, filled in when the command runs. hud?: string; }; @@ -34,23 +35,23 @@ function workspaceSuggestions(workspace: DefiWorkspace, namedCount: number): Sug kind: "workspace", }, { - title: `Move Window to ${label}`, + title: `Move Focused Window to ${label}`, description: `Moves the focused window to ${label} and follows it.`, args: ["move-window-to-workspace-name", workspace.id], binding: target("move-window-to-workspace-name", "move-window-to-workspace-position"), kind: "window", - hud: `Moved window to ${label}`, + hud: `Moved {window} to ${label}`, }, { - title: `Send Window to ${label}`, + title: `Send Focused Window to ${label}`, description: `Moves the focused window to ${label} and stays here.`, args: ["send-window-to-workspace-name", workspace.id], binding: target("send-window-to-workspace-name", "send-window-to-workspace-position"), kind: "window", - hud: `Sent window to ${label}`, + hud: `Sent {window} to ${label}`, }, { - title: `Move Column to ${label}`, + title: `Move Focused Column to ${label}`, description: `Moves the focused column to ${label} and follows it.`, args: ["move-column-to-workspace-name", workspace.id], binding: target("move-column-to-workspace-name", "move-column-to-workspace-position"), @@ -59,7 +60,7 @@ function workspaceSuggestions(workspace: DefiWorkspace, namedCount: number): Sug hud: `Moved column to ${label}`, }, { - title: `Send Column to ${label}`, + title: `Send Focused Column to ${label}`, description: `Moves the focused column to ${label} and stays here.`, args: ["send-column-to-workspace-name", workspace.id], binding: target("send-column-to-workspace-name"), diff --git a/contrib/raycast/src/window-list.tsx b/contrib/raycast/src/window-list.tsx index 6edb99e6..e08c5452 100644 --- a/contrib/raycast/src/window-list.tsx +++ b/contrib/raycast/src/window-list.tsx @@ -7,7 +7,7 @@ import { ErrorView } from "./error-view"; import { workspaceLabel, type DefiWindow, type DefiWorkspace } from "./parse"; import { StripDetail } from "./strip-detail"; import { stripColumns } from "./strip"; -import { workspaceIcon } from "./workspace-icon"; +import { workspaceAccessories, workspaceIcon } from "./workspace-icon"; const current = "current"; @@ -21,27 +21,35 @@ async function readOrReport(read: () => Promise): Promise { } } -function MoveTargets(props: { - workspaces: DefiWorkspace[]; - displayCount: number; +// Where to move a window, read afresh so a workspace that has gone is never offered. +function MoveTargets({ + from, + subject, + onMove, +}: { + from?: string; subject: string; onMove: (workspace: DefiWorkspace) => void; }) { - const displays = [...new Set(props.workspaces.map((workspace) => workspace.display))]; + const { workspaces, isLoading, displayCount, appIcon, appName } = useDesktop(); + const targets = (workspaces ?? []).filter((workspace) => workspace.id !== from); + const displays = [...new Set(targets.map((workspace) => workspace.display))]; return ( - + {displays.map((display) => ( - 1 ? `Display ${display}` : undefined}> - {props.workspaces + 1 ? `Display ${display}` : undefined}> + {targets .filter((workspace) => workspace.display === display) .map((workspace) => ( - props.onMove(workspace)} /> + onMove(workspace)} /> } /> @@ -191,33 +199,31 @@ export function WindowList({ initialSearch = "", workspaceID }: { initialSearch? ] } detail={ - workspace ? ( - - {window.title ? : null} - - - {displayCount > 1 ? ( - - ) : null} - - {window.focused ? ( - - - - ) : null} - - ) : undefined + + {window.title ? : null} + + {label ? : null} + {displayCount > 1 ? ( + + ) : null} + + {window.focused ? ( + + + + ) : null} + } actions={ - + act(window)} /> {/* A pushed list, not a submenu: some launchers flatten submenus, which would give ⌘↵ to its first entry. */} target.id !== workspace?.id)} - displayCount={displayCount} - subject={window.appName} + from={workspace?.id} + subject={window.title || window.appName} onMove={(target) => act( window, @@ -266,7 +271,7 @@ export function WindowList({ initialSearch = "", workspaceID }: { initialSearch? } /> - + = { @@ -53,3 +53,20 @@ export function workspaceIcon(workspace: Workspace, active = false): Image.Image const source = (symbol && (symbols[symbol] ?? symbols[symbol.split(".")[0]])) || number || Icon.AppWindowGrid3x3; return active ? { source, tintColor: Color.Blue } : source; } + +const maxAppIcons = 5; + +// A workspace row's accessories: up to five app icons, a count of the rest, and the Active tag. +export function workspaceAccessories( + workspace: { applications: string[]; active: boolean }, + appIcon: (bundleID?: string) => Image.ImageLike | undefined, + appName: (bundleID: string) => string, +): List.Item.Accessory[] { + const icons = workspace.applications.map((app) => ({ icon: appIcon(app) ?? Icon.AppWindow, tooltip: appName(app) })); + const extra = icons.length - maxAppIcons; + return [ + ...icons.slice(0, maxAppIcons), + ...(extra > 0 ? [{ text: `+${extra}` }] : []), + ...(workspace.active ? [{ tag: { value: "Active", color: Color.Blue } }] : []), + ]; +} diff --git a/contrib/raycast/test/view.test.ts b/contrib/raycast/test/view.test.ts index 74b78cbc..737cecc8 100644 --- a/contrib/raycast/test/view.test.ts +++ b/contrib/raycast/test/view.test.ts @@ -36,7 +36,7 @@ test("search text becomes a command argument", () => { assert.deepEqual(titles("60"), [["Set Column Width to 60%", "set-column-width 60%"]]); assert.deepEqual(titles("3"), [ ["Go to dev-secondary", "--monitor 1 workspace dev-secondary"], - ["Move Window to dev-secondary", "move-window-to-workspace-name dev-secondary"], + ["Move Focused Window to dev-secondary", "move-window-to-workspace-name dev-secondary"], ]); assert.deepEqual(titles("7"), [["Set Column Width to 7%", "set-column-width 7%"]]); assert.deepEqual(titles("+10"), [["Widen Column by 10%", "set-column-width +10%"]]); @@ -47,13 +47,19 @@ test("search text becomes a command argument", () => { // A partial name offers the two common actions, a word inside the name counts, one letter offers none. assert.deepEqual(titles("we"), [ ["Go to web", "--monitor 1 workspace web"], - ["Move Window to web", "move-window-to-workspace-name web"], + ["Move Focused Window to web", "move-window-to-workspace-name web"], ]); assert.equal(titles("sec")[0][0], "Go to dev-secondary"); assert.deepEqual(titles("w"), []); assert.deepEqual( titles("web").map(([title]) => title), - ["Go to web", "Move Window to web", "Send Window to web", "Move Column to web", "Send Column to web"], + [ + "Go to web", + "Move Focused Window to web", + "Send Focused Window to web", + "Move Focused Column to web", + "Send Focused Column to web", + ], ); assert.deepEqual(titles("zzz"), []); assert.deepEqual(titles(" "), []); From db0d6bcef621b785180679c159001ec5ee7db2c1 Mon Sep 17 00:00:00 2001 From: Quentin Eude Date: Wed, 11 Nov 2026 15:23:37 +0100 Subject: [PATCH 13/23] fix(contrib): rebuild recent suggestions from live state in Raycast Go to never second-guesses cached state, the move targets show errors and search apps, recent workspace suggestions are worded again from the live workspaces, the Search Commands pane gives the [keys] line, argument rows show examples, and the README lists each command's keys. --- contrib/raycast/CHANGELOG.md | 2 +- contrib/raycast/README.md | 72 ++++++++++++++--------- contrib/raycast/package.json | 2 +- contrib/raycast/src/commands.ts | 4 ++ contrib/raycast/src/run-command.tsx | 47 ++++++++++----- contrib/raycast/src/search-workspaces.tsx | 17 +++--- contrib/raycast/src/suggest.ts | 5 ++ contrib/raycast/src/window-list.tsx | 14 +++-- 8 files changed, 105 insertions(+), 58 deletions(-) diff --git a/contrib/raycast/CHANGELOG.md b/contrib/raycast/CHANGELOG.md index bc9209e4..37986a37 100644 --- a/contrib/raycast/CHANGELOG.md +++ b/contrib/raycast/CHANGELOG.md @@ -2,6 +2,6 @@ ## [Initial Version] - 2026-11-11 -- Search Windows: focus any managed window, bring it to the current workspace, move it, float it, or copy its title. +- Search Windows: focus any managed window, bring it to the current workspace, move it, float it, or copy its title or ID. - Search Workspaces: jump to a workspace on any display, or move the focused window or column there. - Search Commands: run any Defi command; the search text sets widths and heights and targets workspaces. diff --git a/contrib/raycast/README.md b/contrib/raycast/README.md index 06161e58..296b6a92 100644 --- a/contrib/raycast/README.md +++ b/contrib/raycast/README.md @@ -8,33 +8,49 @@ titles stay on your Mac. ## Commands -- **Search Windows** finds any window Defi manages by title, app, or - workspace, and the dropdown narrows it to one workspace. Enter focuses the - window, ⌘↵ picks a workspace to move it to, ⇧↵ brings it to the current one, - and ⌘⇧F floats or tiles it. Windows you pick often come first; the focused - one comes last. Acting on a window from another workspace shows that - workspace for a moment, because Defi focuses the window first. -- **Search Workspaces** lists the workspaces of every display with their apps - and strip; search by name, number, or app. Enter goes to the workspace. ⌘↵ - moves the focused window there, ⌘⇧↵ sends it without following, and ⌥↵ and - ⌥⇧↵ do the same with its column. ⌘O lists the workspace's windows. -- **Search Commands** runs any Defi command, with the ones you ran last on top. - The side pane says what each one does, and the row shows its default - shortcut. The search text also works as an argument: `60` sets the column - width to 60%, `+10` or `-10` changes it, `960` sets it in points, `h50` sets a - stacked window's height, `2` goes to the second workspace, and a workspace - name offers to go there or move the focused window or column there. Type the - same text after the command in the root search, as in `Search Commands 60`. - ⌘↵ copies a command in the form `[keys]` accepts. See - [Commands](https://github.com/qeude/Defi/blob/main/CONFIGURATION.md#commands). - -⌘Y shows or hides the side pane, and ⌘R reloads a list. -Shortcuts shown are Defi's defaults, with `mod` as ⌃⌥; your own `[keys]` may -differ. +**Search Windows** finds any window Defi manages by title, app, or workspace; +the dropdown narrows it to one workspace. Windows you pick often come first, +and the focused one comes last. Acting on a window from another workspace shows +that workspace for a moment, because Defi focuses the window first. + +| Key | Action | +| --- | --- | +| ↵ | Focus the window | +| ⌘↵ | Pick a workspace to move it to | +| ⇧↵ | Bring it to the current workspace | +| ⌘⇧F | Float or tile it | +| ⌘⇧C / ⌘⇧I | Copy its title or ID | + +**Search Workspaces** lists the workspaces of every display with their apps and +strip; search by name, number, or app. + +| Key | Action | +| --- | --- | +| ↵ | Go to the workspace | +| ⌘↵ / ⌘⇧↵ | Move the focused window there, or send it without following | +| ⌥↵ / ⌥⇧↵ | Move or send the focused column | +| ⌘O | List the workspace's windows | + +**Search Commands** runs any Defi command, with the ones you ran last on top. +The side pane says what each one does, its default shortcut, and the line to +paste into `[keys]`. The search text also works as an argument: + +| Type | To | +| --- | --- | +| `60`, `+10`, `-10`, `960` | Set or change the column width, in percent or points | +| `h50`, `h+10` | Set or change a stacked window's height | +| `2`, `web` | Go to a workspace, or move the focused window or column there | + +Type the same text after the command in the root search, as in +`Search Commands 60`. ⌘↵ copies a command in the form `[keys]` accepts. See +[Commands](https://github.com/qeude/Defi/blob/main/CONFIGURATION.md#commands). + +In every list, ⌘Y shows or hides the side pane and ⌘R reloads. Shortcuts shown +are Defi's defaults, with `mod` as ⌃⌥; your own `[keys]` may differ. ## Install -You need Node.js 22 or later and Defi.app. From the Defi repository root: +You need Node.js 22.6 or later and Defi.app. From the Defi repository root: ```sh cd contrib/raycast @@ -43,9 +59,11 @@ npm install && npm run dev `npm run dev` adds the extension to Raycast in development mode. Give Search Windows a hotkey or an alias such as `w` in its command settings (⌘K, then -Configure Command) to reach it in one keystroke. For Tinycast, -run `npm run tinycast` instead: it builds straight into Tinycast's extensions -folder, and Tinycast picks up a new build the next time a command starts. +Configure Command) to reach it in one keystroke. + +For Tinycast, run `npm run tinycast` instead: it builds straight into Tinycast's +extensions folder, and Tinycast picks up a new build the next time a command +starts. ## Troubleshooting diff --git a/contrib/raycast/package.json b/contrib/raycast/package.json index 792ebf75..d5c8cb80 100644 --- a/contrib/raycast/package.json +++ b/contrib/raycast/package.json @@ -90,6 +90,6 @@ "fix-lint": "ray lint --fix", "lint": "ray lint", "tinycast": "ray build -e dist -o \"$HOME/Library/Application Support/com.tinycast.app/extensions/defi\"", - "test": "node --test test/" + "test": "node --experimental-strip-types --test test/" } } diff --git a/contrib/raycast/src/commands.ts b/contrib/raycast/src/commands.ts index 1f5a90b0..7eac9ed3 100644 --- a/contrib/raycast/src/commands.ts +++ b/contrib/raycast/src/commands.ts @@ -12,6 +12,7 @@ export const prompts: { example: string; // The command in [keys] syntax, with a placeholder for the value. syntax: string; + examples: string; hint: string; keywords: string[]; }[] = [ @@ -21,6 +22,7 @@ export const prompts: { icon: "ArrowRight", example: "1", syntax: "workspace ", + examples: "2, web", hint: "Type a workspace's number or the start of its name, such as 2 or web.", keywords: ["workspace", "go", "switch", "number", "name"], }, @@ -30,6 +32,7 @@ export const prompts: { icon: "ArrowsExpand", example: "60", syntax: "set-column-width ", + examples: "60, +10, 960", hint: "Type a width: 60 for 60%, +10 or -10 to change it, or 960 for points.", keywords: ["width", "resize", "size", "column"], }, @@ -39,6 +42,7 @@ export const prompts: { icon: "ChevronUpDown", example: "h50", syntax: "set-window-height ", + examples: "h50, h+10", hint: "Type h and a height for the focused stacked window: h50, h+10, or h-10.", keywords: ["height", "stack", "resize", "size"], }, diff --git a/contrib/raycast/src/run-command.tsx b/contrib/raycast/src/run-command.tsx index 26291bc6..d03fa547 100644 --- a/contrib/raycast/src/run-command.tsx +++ b/contrib/raycast/src/run-command.tsx @@ -6,12 +6,21 @@ import { runAndClose } from "./defi"; import { useDesktop } from "./desktop"; import { describeError, RecoveryActions } from "./error-view"; import { formatShortcut } from "./shortcut"; -import { suggestions, type Suggestion } from "./suggest"; +import { forWorkspace, suggestions, type Suggestion } from "./suggest"; const entries = sections.flatMap(([section, list]) => list.map((entry) => ({ ...entry, section }))); const recentLimit = 5; -type Recent = { command: string } | { suggestion: Suggestion }; +// A workspace suggestion is kept as its workspace and action, and worded again from the live state, +// so a reordered or moved workspace never shows a stale label or display. +type Recent = { command: string } | { suggestion: Suggestion } | { workspaceID: string; action: string }; + +const recentKey = (run: Recent) => + "command" in run + ? run.command + : "suggestion" in run + ? run.suggestion.args.join(" ") + : `${run.action} ${run.workspaceID}`; const suggestionIcons: Record = { width: Icon.ArrowsExpand, @@ -21,11 +30,13 @@ const suggestionIcons: Record = { window: Icon.Window, }; -// What a row does; Default Shortcut appears where Defi binds one or for a command that has none. +// What a row does, then the line to paste into [keys]; Default Shortcut appears where Defi binds +// one, or as None for a command that has none. function CommandDetail({ text, command, shortcut }: { text: string; command?: string; shortcut?: string | null }) { + const binding = command ? `\n\n\`\`\`toml\n"${shortcut ?? "mod-x"}" = "${command}"\n\`\`\`` : ""; return ( @@ -51,19 +62,20 @@ export default function Command(props: LaunchProps<{ arguments: Arguments.RunCom const desktop = useDesktop(); const problem = desktop.error; const suggested = suggestions(searchText, problem ? [] : (desktop.workspaces ?? [])); - const workspaceIDs = new Set(desktop.workspaces?.map((workspace) => workspace.id)); - // A remembered move to a workspace that has since gone is dropped rather than offered. + const live = desktop.workspaces ?? []; const recentRows = searchText ? [] - : recent.filter( - (run) => - "command" in run || - !run.suggestion.args.some((arg) => arg.startsWith("__defi_dynamic_") && !workspaceIDs.has(arg)), - ); + : recent.flatMap((run): (Suggestion | string)[] => { + if ("command" in run) return [run.command]; + if ("suggestion" in run) return [run.suggestion]; + // A remembered move to a workspace that has since gone is dropped rather than offered. + const workspace = live.find((candidate) => candidate.id === run.workspaceID); + const suggestion = workspace && forWorkspace(workspace, live).find((s) => s.args[0] === run.action); + return suggestion ? [suggestion] : []; + }); const remember = (run: Recent) => { - const key = (other: Recent) => ("command" in other ? other.command : other.suggestion.args.join(" ")); - setRecent([run, ...recent.filter((other) => key(other) !== key(run))].slice(0, recentLimit)); + setRecent([run, ...recent.filter((other) => recentKey(other) !== recentKey(run))].slice(0, recentLimit)); }; const run = (entry: (typeof entries)[number]) => { @@ -73,7 +85,9 @@ export default function Command(props: LaunchProps<{ arguments: Arguments.RunCom // Runs a suggestion, naming the window it moves from a fresh read. const runSuggestion = async (suggestion: Suggestion) => { - remember({ suggestion }); + const workspaceID = + suggestion.kind === "width" || suggestion.kind === "height" ? undefined : suggestion.args.at(-1); + remember(workspaceID ? { workspaceID, action: suggestion.args[0] } : { suggestion }); const app = suggestion.hud?.includes("{window}") ? (await desktop.fresh().catch(() => undefined))?.windows.find((window) => window.focused)?.appName : undefined; @@ -176,8 +190,8 @@ export default function Command(props: LaunchProps<{ arguments: Arguments.RunCom {recentRows.length > 0 ? ( {recentRows.map((run) => { - if (!("command" in run)) return suggestionItem(run.suggestion, "recent-"); - const entry = entries.find((candidate) => candidate.command === run.command); + if (typeof run !== "string") return suggestionItem(run, "recent-"); + const entry = entries.find((candidate) => candidate.command === run); return entry ? item(entry, "recent-") : null; })} @@ -194,6 +208,7 @@ export default function Command(props: LaunchProps<{ arguments: Arguments.RunCom key={prompt.title} icon={Icon[prompt.icon]} title={prompt.title} + subtitle={showDetail ? undefined : prompt.examples} keywords={prompt.keywords} detail={} actions={ diff --git a/contrib/raycast/src/search-workspaces.tsx b/contrib/raycast/src/search-workspaces.tsx index d742362c..9fb6f574 100644 --- a/contrib/raycast/src/search-workspaces.tsx +++ b/contrib/raycast/src/search-workspaces.tsx @@ -42,7 +42,12 @@ export default function Command(props: LaunchProps) { } const latest = state.windows.find((window) => window.focused); if (!latest || latest.workspace?.id === workspace.id) { - await showToast({ style: Toast.Style.Failure, title: latest ? `Already on ${label}` : "No Focused Window" }); + // Already there is not a failure; no focused window is. + await showToast( + latest + ? { style: Toast.Style.Success, title: `Already on ${label}` } + : { style: Toast.Style.Failure, title: "No Focused Window" }, + ); return; } await runAndClose([[command, workspace.id]], describe(latest.appName)); @@ -53,11 +58,7 @@ export default function Command(props: LaunchProps) { - workspace.active && workspace.focusedDisplay - ? showToast({ style: Toast.Style.Failure, title: `Already on ${label}` }) - : runAndClose([["--monitor", String(workspace.display), "workspace", workspace.id]]) - } + onAction={() => runAndClose([["--monitor", String(workspace.display), "workspace", workspace.id]])} /> {/* The secondary action keeps ⌘↵ on every row, so it stays even where it has nothing to move. */} `Moved ${app} to ${label}`)} /> - - `Sent ${app} to ${label}`)} /> - + other.kind === "named").length); +} + export function suggestions(query: string, workspaces: DefiWorkspace[]): Suggestion[] { const text = query.trim(); if (!text) return []; diff --git a/contrib/raycast/src/window-list.tsx b/contrib/raycast/src/window-list.tsx index e08c5452..f9097753 100644 --- a/contrib/raycast/src/window-list.tsx +++ b/contrib/raycast/src/window-list.tsx @@ -21,7 +21,8 @@ async function readOrReport(read: () => Promise): Promise { } } -// Where to move a window, read afresh so a workspace that has gone is never offered. +// Where to move a window: cached workspaces at once, then a fresh read; a target that has gone +// in between fails with Defi's own message. function MoveTargets({ from, subject, @@ -31,11 +32,16 @@ function MoveTargets({ subject: string; onMove: (workspace: DefiWorkspace) => void; }) { - const { workspaces, isLoading, displayCount, appIcon, appName } = useDesktop(); - const targets = (workspaces ?? []).filter((workspace) => workspace.id !== from); + const { workspaces, isLoading, error, revalidate, displayCount, appIcon, appName } = useDesktop(); + const targets = error ? [] : (workspaces ?? []).filter((workspace) => workspace.id !== from); const displays = [...new Set(targets.map((workspace) => workspace.display))]; return ( - + + {error ? : null} {displays.map((display) => ( 1 ? `Display ${display}` : undefined}> {targets From fe20e289e5af1499d9b4b58c933030081c8801b3 Mon Sep 17 00:00:00 2001 From: Quentin Eude Date: Wed, 11 Nov 2026 15:25:12 +0100 Subject: [PATCH 14/23] fix(contrib): run the Raycast extension tests on Node 22.6 and mark stand-in keys Glob the .mts tests so Node 22 finds them, silence the typeless-package warning, and say in the pasted [keys] line when its key is a stand-in. --- contrib/raycast/package.json | 2 +- contrib/raycast/src/run-command.tsx | 6 ++++-- .../raycast/test/{commands.test.ts => commands.test.mts} | 0 contrib/raycast/test/{parse.test.ts => parse.test.mts} | 0 contrib/raycast/test/{view.test.ts => view.test.mts} | 0 5 files changed, 5 insertions(+), 3 deletions(-) rename contrib/raycast/test/{commands.test.ts => commands.test.mts} (100%) rename contrib/raycast/test/{parse.test.ts => parse.test.mts} (100%) rename contrib/raycast/test/{view.test.ts => view.test.mts} (100%) diff --git a/contrib/raycast/package.json b/contrib/raycast/package.json index d5c8cb80..96114a90 100644 --- a/contrib/raycast/package.json +++ b/contrib/raycast/package.json @@ -90,6 +90,6 @@ "fix-lint": "ray lint --fix", "lint": "ray lint", "tinycast": "ray build -e dist -o \"$HOME/Library/Application Support/com.tinycast.app/extensions/defi\"", - "test": "node --experimental-strip-types --test test/" + "test": "node --experimental-strip-types --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --test 'test/*.test.mts'" } } diff --git a/contrib/raycast/src/run-command.tsx b/contrib/raycast/src/run-command.tsx index d03fa547..9dfd445a 100644 --- a/contrib/raycast/src/run-command.tsx +++ b/contrib/raycast/src/run-command.tsx @@ -33,14 +33,16 @@ const suggestionIcons: Record = { // What a row does, then the line to paste into [keys]; Default Shortcut appears where Defi binds // one, or as None for a command that has none. function CommandDetail({ text, command, shortcut }: { text: string; command?: string; shortcut?: string | null }) { - const binding = command ? `\n\n\`\`\`toml\n"${shortcut ?? "mod-x"}" = "${command}"\n\`\`\`` : ""; + // Without a default shortcut, the key in the line is a stand-in, and says so. + const key = shortcut ? `"${shortcut}"` : '"mod-x"'; + const note = shortcut ? "" : " # pick your own key"; + const binding = command ? `\n\n\`\`\`toml\n${key} = "${command}"${note}\n\`\`\`` : ""; return ( - {shortcut !== undefined ? ( Date: Wed, 11 Nov 2026 15:27:14 +0100 Subject: [PATCH 15/23] fix(contrib): skip empty metadata in Search Commands and quiet Node 22 tests --- contrib/raycast/package.json | 2 +- contrib/raycast/src/run-command.tsx | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/contrib/raycast/package.json b/contrib/raycast/package.json index 96114a90..0bd4b6dd 100644 --- a/contrib/raycast/package.json +++ b/contrib/raycast/package.json @@ -90,6 +90,6 @@ "fix-lint": "ray lint --fix", "lint": "ray lint", "tinycast": "ray build -e dist -o \"$HOME/Library/Application Support/com.tinycast.app/extensions/defi\"", - "test": "node --experimental-strip-types --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --test 'test/*.test.mts'" + "test": "node --experimental-strip-types --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --disable-warning=ExperimentalWarning --test 'test/*.test.mts'" } } diff --git a/contrib/raycast/src/run-command.tsx b/contrib/raycast/src/run-command.tsx index 9dfd445a..76098af4 100644 --- a/contrib/raycast/src/run-command.tsx +++ b/contrib/raycast/src/run-command.tsx @@ -41,14 +41,12 @@ function CommandDetail({ text, command, shortcut }: { text: string; command?: st - {shortcut !== undefined ? ( - - ) : null} + ) : undefined } From da05d8086596f7dc42cb5d20b30a4381a8f401c3 Mon Sep 17 00:00:00 2001 From: Quentin Eude Date: Wed, 11 Nov 2026 17:12:49 +0100 Subject: [PATCH 16/23] feat(contrib): preview the selected window in Search Windows Capture the selected window with screencapture while the side pane is open, shrink it with sips, and show it as a data URI above the strip; the file is deleted as soon as it is read. --- contrib/raycast/README.md | 6 +++++- contrib/raycast/src/preview.ts | 28 ++++++++++++++++++++++++++++ contrib/raycast/src/strip-detail.tsx | 8 ++++++-- contrib/raycast/src/window-list.tsx | 14 +++++++++++++- 4 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 contrib/raycast/src/preview.ts diff --git a/contrib/raycast/README.md b/contrib/raycast/README.md index 296b6a92..2568ac4b 100644 --- a/contrib/raycast/README.md +++ b/contrib/raycast/README.md @@ -45,7 +45,11 @@ Type the same text after the command in the root search, as in `Search Commands 60`. ⌘↵ copies a command in the form `[keys]` accepts. See [Commands](https://github.com/qeude/Defi/blob/main/CONFIGURATION.md#commands). -In every list, ⌘Y shows or hides the side pane and ⌘R reloads. Shortcuts shown +In every list, ⌘Y shows or hides the side pane and ⌘R reloads. In Search +Windows, the pane also shows a picture of the selected window, taken with +macOS's `screencapture` when you select it; Raycast or Tinycast asks once for +Screen Recording. The picture stays in memory and is never written to disk +beyond the moment it is read. Shortcuts shown are Defi's defaults, with `mod` as ⌃⌥; your own `[keys]` may differ. ## Install diff --git a/contrib/raycast/src/preview.ts b/contrib/raycast/src/preview.ts new file mode 100644 index 00000000..581e6e66 --- /dev/null +++ b/contrib/raycast/src/preview.ts @@ -0,0 +1,28 @@ +import { execFile } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const maxSide = "800"; + +function run(file: string, args: string[]): Promise { + return new Promise((resolve, reject) => + execFile(file, args, { timeout: 3000 }, (error) => (error ? reject(error) : resolve())), + ); +} + +// A picture of one window as a data URI, or undefined when macOS refuses (no Screen Recording). +// The file only lives while it is read: these are pictures of the user's windows. +export async function capturePreview(windowID: number): Promise { + const folder = mkdtempSync(join(tmpdir(), "defi-preview-")); + const file = join(folder, "window.jpg"); + try { + await run("/usr/sbin/screencapture", ["-x", "-o", "-l", String(windowID), "-t", "jpg", file]); + await run("/usr/bin/sips", ["-Z", maxSide, "-s", "formatOptions", "70", file]); + return `data:image/jpeg;base64,${readFileSync(file).toString("base64")}`; + } catch { + return undefined; + } finally { + rmSync(folder, { recursive: true, force: true }); + } +} diff --git a/contrib/raycast/src/strip-detail.tsx b/contrib/raycast/src/strip-detail.tsx index 45fa5c26..892f5b40 100644 --- a/contrib/raycast/src/strip-detail.tsx +++ b/contrib/raycast/src/strip-detail.tsx @@ -9,13 +9,15 @@ type Props = { dark: boolean; appIcon: (bundleID?: string) => Image.ImageLike | undefined; empty: string; + // A picture of the highlighted window, shown above its strip. + preview?: string; children?: React.ReactNode; }; const maxTagLength = 24; // The workspace's strip, then the caller's rows, then its columns named in order. -export function StripDetail({ windows, workspaceID, highlightID, dark, appIcon, empty, children }: Props) { +export function StripDetail({ windows, workspaceID, highlightID, dark, appIcon, empty, preview, children }: Props) { const columns = stripColumns(windows, workspaceID); const floating = windows.filter((window) => window.workspace?.id === workspaceID && window.floating); const onWorkspace = [...columns.flat(), ...floating]; @@ -36,7 +38,9 @@ export function StripDetail({ windows, workspaceID, highlightID, dark, appIcon, )); return ( {children} diff --git a/contrib/raycast/src/window-list.tsx b/contrib/raycast/src/window-list.tsx index f9097753..7a4d8f5e 100644 --- a/contrib/raycast/src/window-list.tsx +++ b/contrib/raycast/src/window-list.tsx @@ -1,11 +1,12 @@ import { Action, ActionPanel, Color, environment, Icon, Keyboard, List } from "@raycast/api"; import { showFailureToast, useCachedState, useFrecencySorting } from "@raycast/utils"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { runAndClose } from "./defi"; import { useDesktop } from "./desktop"; import { ErrorView } from "./error-view"; import { workspaceLabel, type DefiWindow, type DefiWorkspace } from "./parse"; import { StripDetail } from "./strip-detail"; +import { capturePreview } from "./preview"; import { stripColumns } from "./strip"; import { workspaceAccessories, workspaceIcon } from "./workspace-icon"; @@ -86,6 +87,14 @@ export function WindowList({ initialSearch = "", workspaceID }: { initialSearch? // The focused window goes last: Enter on it would change nothing. const sorted = [...inScope.filter((window) => !window.focused), ...inScope.filter((window) => window.focused)]; const dark = environment.appearance === "dark"; + // Pictures of windows, taken one at a time for the selected row while the side pane is open. + const [selectedID, setSelectedID] = useState(null); + const [previews, setPreviews] = useState>({}); + useEffect(() => { + if (!showDetail || !selectedID || selectedID in previews) return; + setPreviews((taken) => ({ ...taken, [selectedID]: undefined })); + capturePreview(Number(selectedID)).then((preview) => setPreviews((taken) => ({ ...taken, [selectedID]: preview }))); + }, [showDetail, selectedID]); const position = (window: DefiWindow) => { if (!window.workspace || !window.column) return "Floating"; @@ -166,6 +175,7 @@ export function WindowList({ initialSearch = "", workspaceID }: { initialSearch? searchBarAccessory={scopeMenu} searchText={searchText} onSearchTextChange={setSearchText} + onSelectionChange={setSelectedID} filtering > {emptyView()} @@ -176,6 +186,7 @@ export function WindowList({ initialSearch = "", workspaceID }: { initialSearch? return ( {window.title ? : null} From 50f85da4c7a68c213d4f15e9ea27b60e88eeb518 Mon Sep 17 00:00:00 2001 From: Quentin Eude Date: Wed, 11 Nov 2026 17:16:02 +0100 Subject: [PATCH 17/23] fix(contrib): say when a window picture is refused and link Screen Recording A launcher without Screen Recording got no picture and no explanation; the pane now says so and offers to open the privacy setting. --- contrib/raycast/src/strip-detail.tsx | 11 ++++++++--- contrib/raycast/src/window-list.tsx | 15 +++++++++++++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/contrib/raycast/src/strip-detail.tsx b/contrib/raycast/src/strip-detail.tsx index 892f5b40..5d24b3d6 100644 --- a/contrib/raycast/src/strip-detail.tsx +++ b/contrib/raycast/src/strip-detail.tsx @@ -9,8 +9,8 @@ type Props = { dark: boolean; appIcon: (bundleID?: string) => Image.ImageLike | undefined; empty: string; - // A picture of the highlighted window, shown above its strip. - preview?: string; + // A picture of the highlighted window above its strip; null when macOS refused to take it. + preview?: string | null; children?: React.ReactNode; }; @@ -38,7 +38,12 @@ export function StripDetail({ windows, workspaceID, highlightID, dark, appIcon, )); return ( (read: () => Promise): Promise { @@ -89,11 +90,14 @@ export function WindowList({ initialSearch = "", workspaceID }: { initialSearch? const dark = environment.appearance === "dark"; // Pictures of windows, taken one at a time for the selected row while the side pane is open. const [selectedID, setSelectedID] = useState(null); - const [previews, setPreviews] = useState>({}); + // undefined while a picture is being taken, null when macOS refused it. + const [previews, setPreviews] = useState>({}); useEffect(() => { if (!showDetail || !selectedID || selectedID in previews) return; setPreviews((taken) => ({ ...taken, [selectedID]: undefined })); - capturePreview(Number(selectedID)).then((preview) => setPreviews((taken) => ({ ...taken, [selectedID]: preview }))); + capturePreview(Number(selectedID)).then((preview) => + setPreviews((taken) => ({ ...taken, [selectedID]: preview ?? null })), + ); }, [showDetail, selectedID]); const position = (window: DefiWindow) => { @@ -314,6 +318,13 @@ export function WindowList({ initialSearch = "", workspaceID }: { initialSearch? shortcut={Keyboard.Shortcut.Common.Refresh} onAction={desktop.revalidate} /> + {previews[String(window.id)] === null ? ( + + ) : null} } From c908e266adecfd616c3dd865cd5cf6797146993c Mon Sep 17 00:00:00 2001 From: Quentin Eude Date: Wed, 11 Nov 2026 17:23:27 +0100 Subject: [PATCH 18/23] feat(contrib): show Defi's own window pictures instead of capturing Read each picture with defi window-preview, so the launcher captures nothing and needs no Screen Recording; an older Defi gets a hint to update. --- contrib/raycast/README.md | 7 +++--- contrib/raycast/src/preview.ts | 32 ++++++++-------------------- contrib/raycast/src/strip-detail.tsx | 9 ++++---- contrib/raycast/src/window-list.tsx | 19 ++++------------- 4 files changed, 20 insertions(+), 47 deletions(-) diff --git a/contrib/raycast/README.md b/contrib/raycast/README.md index 2568ac4b..1b72a92c 100644 --- a/contrib/raycast/README.md +++ b/contrib/raycast/README.md @@ -46,10 +46,9 @@ Type the same text after the command in the root search, as in [Commands](https://github.com/qeude/Defi/blob/main/CONFIGURATION.md#commands). In every list, ⌘Y shows or hides the side pane and ⌘R reloads. In Search -Windows, the pane also shows a picture of the selected window, taken with -macOS's `screencapture` when you select it; Raycast or Tinycast asks once for -Screen Recording. The picture stays in memory and is never written to disk -beyond the moment it is read. Shortcuts shown +Windows, the pane also shows Defi's latest picture of the selected window, read +with `defi window-preview`: the launcher captures nothing and needs no Screen +Recording of its own. Shortcuts shown are Defi's defaults, with `mod` as ⌃⌥; your own `[keys]` may differ. ## Install diff --git a/contrib/raycast/src/preview.ts b/contrib/raycast/src/preview.ts index 581e6e66..3fb4ebfe 100644 --- a/contrib/raycast/src/preview.ts +++ b/contrib/raycast/src/preview.ts @@ -1,28 +1,14 @@ -import { execFile } from "node:child_process"; -import { mkdtempSync, readFileSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { defi } from "./defi"; -const maxSide = "800"; +export type Preview = { picture: string } | { problem: string }; -function run(file: string, args: string[]): Promise { - return new Promise((resolve, reject) => - execFile(file, args, { timeout: 3000 }, (error) => (error ? reject(error) : resolve())), - ); -} - -// A picture of one window as a data URI, or undefined when macOS refuses (no Screen Recording). -// The file only lives while it is read: these are pictures of the user's windows. -export async function capturePreview(windowID: number): Promise { - const folder = mkdtempSync(join(tmpdir(), "defi-preview-")); - const file = join(folder, "window.jpg"); +// Defi's own latest picture of the window: the launcher captures nothing and needs no permission. +export async function windowPreview(windowID: number): Promise { try { - await run("/usr/sbin/screencapture", ["-x", "-o", "-l", String(windowID), "-t", "jpg", file]); - await run("/usr/bin/sips", ["-Z", maxSide, "-s", "formatOptions", "70", file]); - return `data:image/jpeg;base64,${readFileSync(file).toString("base64")}`; - } catch { - return undefined; - } finally { - rmSync(folder, { recursive: true, force: true }); + return { picture: (await defi("window-preview", String(windowID))).trim() }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + // Defi words its own refusals; anything else is a Defi older than window-preview. + return { problem: message.startsWith("Defi") ? message : "Update Defi to see window pictures." }; } } diff --git a/contrib/raycast/src/strip-detail.tsx b/contrib/raycast/src/strip-detail.tsx index 5d24b3d6..76e1547e 100644 --- a/contrib/raycast/src/strip-detail.tsx +++ b/contrib/raycast/src/strip-detail.tsx @@ -1,5 +1,6 @@ import { Color, Image, List } from "@raycast/api"; import type { DefiWindow } from "./parse"; +import type { Preview } from "./preview"; import { stripColumns, stripImage } from "./strip"; type Props = { @@ -9,8 +10,8 @@ type Props = { dark: boolean; appIcon: (bundleID?: string) => Image.ImageLike | undefined; empty: string; - // A picture of the highlighted window above its strip; null when macOS refused to take it. - preview?: string | null; + // Defi's picture of the highlighted window above its strip, or why there is none. + preview?: Preview; children?: React.ReactNode; }; @@ -39,9 +40,7 @@ export function StripDetail({ windows, workspaceID, highlightID, dark, appIcon, return ( (read: () => Promise): Promise { @@ -88,16 +87,13 @@ export function WindowList({ initialSearch = "", workspaceID }: { initialSearch? // The focused window goes last: Enter on it would change nothing. const sorted = [...inScope.filter((window) => !window.focused), ...inScope.filter((window) => window.focused)]; const dark = environment.appearance === "dark"; - // Pictures of windows, taken one at a time for the selected row while the side pane is open. const [selectedID, setSelectedID] = useState(null); - // undefined while a picture is being taken, null when macOS refused it. - const [previews, setPreviews] = useState>({}); + // Defi's pictures of windows, asked one at a time for the selected row while the side pane is open. + const [previews, setPreviews] = useState>({}); useEffect(() => { if (!showDetail || !selectedID || selectedID in previews) return; setPreviews((taken) => ({ ...taken, [selectedID]: undefined })); - capturePreview(Number(selectedID)).then((preview) => - setPreviews((taken) => ({ ...taken, [selectedID]: preview ?? null })), - ); + windowPreview(Number(selectedID)).then((preview) => setPreviews((taken) => ({ ...taken, [selectedID]: preview }))); }, [showDetail, selectedID]); const position = (window: DefiWindow) => { @@ -318,13 +314,6 @@ export function WindowList({ initialSearch = "", workspaceID }: { initialSearch? shortcut={Keyboard.Shortcut.Common.Refresh} onAction={desktop.revalidate} /> - {previews[String(window.id)] === null ? ( - - ) : null} } From dbfe98d4f64443d72a5968d84071a0e02ca0124b Mon Sep 17 00:00:00 2001 From: Quentin Eude Date: Wed, 11 Nov 2026 17:26:11 +0100 Subject: [PATCH 19/23] Revert "feat(contrib): show Defi's own window pictures instead of capturing" This reverts commit c908e266adecfd616c3dd865cd5cf6797146993c. --- contrib/raycast/README.md | 7 +++--- contrib/raycast/src/preview.ts | 32 ++++++++++++++++++++-------- contrib/raycast/src/strip-detail.tsx | 9 ++++---- contrib/raycast/src/window-list.tsx | 19 +++++++++++++---- 4 files changed, 47 insertions(+), 20 deletions(-) diff --git a/contrib/raycast/README.md b/contrib/raycast/README.md index 1b72a92c..2568ac4b 100644 --- a/contrib/raycast/README.md +++ b/contrib/raycast/README.md @@ -46,9 +46,10 @@ Type the same text after the command in the root search, as in [Commands](https://github.com/qeude/Defi/blob/main/CONFIGURATION.md#commands). In every list, ⌘Y shows or hides the side pane and ⌘R reloads. In Search -Windows, the pane also shows Defi's latest picture of the selected window, read -with `defi window-preview`: the launcher captures nothing and needs no Screen -Recording of its own. Shortcuts shown +Windows, the pane also shows a picture of the selected window, taken with +macOS's `screencapture` when you select it; Raycast or Tinycast asks once for +Screen Recording. The picture stays in memory and is never written to disk +beyond the moment it is read. Shortcuts shown are Defi's defaults, with `mod` as ⌃⌥; your own `[keys]` may differ. ## Install diff --git a/contrib/raycast/src/preview.ts b/contrib/raycast/src/preview.ts index 3fb4ebfe..581e6e66 100644 --- a/contrib/raycast/src/preview.ts +++ b/contrib/raycast/src/preview.ts @@ -1,14 +1,28 @@ -import { defi } from "./defi"; +import { execFile } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; -export type Preview = { picture: string } | { problem: string }; +const maxSide = "800"; -// Defi's own latest picture of the window: the launcher captures nothing and needs no permission. -export async function windowPreview(windowID: number): Promise { +function run(file: string, args: string[]): Promise { + return new Promise((resolve, reject) => + execFile(file, args, { timeout: 3000 }, (error) => (error ? reject(error) : resolve())), + ); +} + +// A picture of one window as a data URI, or undefined when macOS refuses (no Screen Recording). +// The file only lives while it is read: these are pictures of the user's windows. +export async function capturePreview(windowID: number): Promise { + const folder = mkdtempSync(join(tmpdir(), "defi-preview-")); + const file = join(folder, "window.jpg"); try { - return { picture: (await defi("window-preview", String(windowID))).trim() }; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - // Defi words its own refusals; anything else is a Defi older than window-preview. - return { problem: message.startsWith("Defi") ? message : "Update Defi to see window pictures." }; + await run("/usr/sbin/screencapture", ["-x", "-o", "-l", String(windowID), "-t", "jpg", file]); + await run("/usr/bin/sips", ["-Z", maxSide, "-s", "formatOptions", "70", file]); + return `data:image/jpeg;base64,${readFileSync(file).toString("base64")}`; + } catch { + return undefined; + } finally { + rmSync(folder, { recursive: true, force: true }); } } diff --git a/contrib/raycast/src/strip-detail.tsx b/contrib/raycast/src/strip-detail.tsx index 76e1547e..5d24b3d6 100644 --- a/contrib/raycast/src/strip-detail.tsx +++ b/contrib/raycast/src/strip-detail.tsx @@ -1,6 +1,5 @@ import { Color, Image, List } from "@raycast/api"; import type { DefiWindow } from "./parse"; -import type { Preview } from "./preview"; import { stripColumns, stripImage } from "./strip"; type Props = { @@ -10,8 +9,8 @@ type Props = { dark: boolean; appIcon: (bundleID?: string) => Image.ImageLike | undefined; empty: string; - // Defi's picture of the highlighted window above its strip, or why there is none. - preview?: Preview; + // A picture of the highlighted window above its strip; null when macOS refused to take it. + preview?: string | null; children?: React.ReactNode; }; @@ -40,7 +39,9 @@ export function StripDetail({ windows, workspaceID, highlightID, dark, appIcon, return ( (read: () => Promise): Promise { @@ -87,13 +88,16 @@ export function WindowList({ initialSearch = "", workspaceID }: { initialSearch? // The focused window goes last: Enter on it would change nothing. const sorted = [...inScope.filter((window) => !window.focused), ...inScope.filter((window) => window.focused)]; const dark = environment.appearance === "dark"; + // Pictures of windows, taken one at a time for the selected row while the side pane is open. const [selectedID, setSelectedID] = useState(null); - // Defi's pictures of windows, asked one at a time for the selected row while the side pane is open. - const [previews, setPreviews] = useState>({}); + // undefined while a picture is being taken, null when macOS refused it. + const [previews, setPreviews] = useState>({}); useEffect(() => { if (!showDetail || !selectedID || selectedID in previews) return; setPreviews((taken) => ({ ...taken, [selectedID]: undefined })); - windowPreview(Number(selectedID)).then((preview) => setPreviews((taken) => ({ ...taken, [selectedID]: preview }))); + capturePreview(Number(selectedID)).then((preview) => + setPreviews((taken) => ({ ...taken, [selectedID]: preview ?? null })), + ); }, [showDetail, selectedID]); const position = (window: DefiWindow) => { @@ -314,6 +318,13 @@ export function WindowList({ initialSearch = "", workspaceID }: { initialSearch? shortcut={Keyboard.Shortcut.Common.Refresh} onAction={desktop.revalidate} /> + {previews[String(window.id)] === null ? ( + + ) : null} } From 2b6f20ee1f5cf3f849e6f18740fe9bbf42d2536d Mon Sep 17 00:00:00 2001 From: Quentin Eude Date: Wed, 11 Nov 2026 17:26:11 +0100 Subject: [PATCH 20/23] Revert "fix(contrib): say when a window picture is refused and link Screen Recording" This reverts commit 50f85da4c7a68c213d4f15e9ea27b60e88eeb518. --- contrib/raycast/src/strip-detail.tsx | 11 +++-------- contrib/raycast/src/window-list.tsx | 15 ++------------- 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/contrib/raycast/src/strip-detail.tsx b/contrib/raycast/src/strip-detail.tsx index 5d24b3d6..892f5b40 100644 --- a/contrib/raycast/src/strip-detail.tsx +++ b/contrib/raycast/src/strip-detail.tsx @@ -9,8 +9,8 @@ type Props = { dark: boolean; appIcon: (bundleID?: string) => Image.ImageLike | undefined; empty: string; - // A picture of the highlighted window above its strip; null when macOS refused to take it. - preview?: string | null; + // A picture of the highlighted window, shown above its strip. + preview?: string; children?: React.ReactNode; }; @@ -38,12 +38,7 @@ export function StripDetail({ windows, workspaceID, highlightID, dark, appIcon, )); return ( (read: () => Promise): Promise { @@ -90,14 +89,11 @@ export function WindowList({ initialSearch = "", workspaceID }: { initialSearch? const dark = environment.appearance === "dark"; // Pictures of windows, taken one at a time for the selected row while the side pane is open. const [selectedID, setSelectedID] = useState(null); - // undefined while a picture is being taken, null when macOS refused it. - const [previews, setPreviews] = useState>({}); + const [previews, setPreviews] = useState>({}); useEffect(() => { if (!showDetail || !selectedID || selectedID in previews) return; setPreviews((taken) => ({ ...taken, [selectedID]: undefined })); - capturePreview(Number(selectedID)).then((preview) => - setPreviews((taken) => ({ ...taken, [selectedID]: preview ?? null })), - ); + capturePreview(Number(selectedID)).then((preview) => setPreviews((taken) => ({ ...taken, [selectedID]: preview }))); }, [showDetail, selectedID]); const position = (window: DefiWindow) => { @@ -318,13 +314,6 @@ export function WindowList({ initialSearch = "", workspaceID }: { initialSearch? shortcut={Keyboard.Shortcut.Common.Refresh} onAction={desktop.revalidate} /> - {previews[String(window.id)] === null ? ( - - ) : null} } From 49d42c25aa00ba3553ccfdad9f15f339c979359f Mon Sep 17 00:00:00 2001 From: Quentin Eude Date: Wed, 11 Nov 2026 17:26:11 +0100 Subject: [PATCH 21/23] Revert "feat(contrib): preview the selected window in Search Windows" This reverts commit da05d8086596f7dc42cb5d20b30a4381a8f401c3. --- contrib/raycast/README.md | 6 +----- contrib/raycast/src/preview.ts | 28 ---------------------------- contrib/raycast/src/strip-detail.tsx | 8 ++------ contrib/raycast/src/window-list.tsx | 14 +------------- 4 files changed, 4 insertions(+), 52 deletions(-) delete mode 100644 contrib/raycast/src/preview.ts diff --git a/contrib/raycast/README.md b/contrib/raycast/README.md index 2568ac4b..296b6a92 100644 --- a/contrib/raycast/README.md +++ b/contrib/raycast/README.md @@ -45,11 +45,7 @@ Type the same text after the command in the root search, as in `Search Commands 60`. ⌘↵ copies a command in the form `[keys]` accepts. See [Commands](https://github.com/qeude/Defi/blob/main/CONFIGURATION.md#commands). -In every list, ⌘Y shows or hides the side pane and ⌘R reloads. In Search -Windows, the pane also shows a picture of the selected window, taken with -macOS's `screencapture` when you select it; Raycast or Tinycast asks once for -Screen Recording. The picture stays in memory and is never written to disk -beyond the moment it is read. Shortcuts shown +In every list, ⌘Y shows or hides the side pane and ⌘R reloads. Shortcuts shown are Defi's defaults, with `mod` as ⌃⌥; your own `[keys]` may differ. ## Install diff --git a/contrib/raycast/src/preview.ts b/contrib/raycast/src/preview.ts deleted file mode 100644 index 581e6e66..00000000 --- a/contrib/raycast/src/preview.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { execFile } from "node:child_process"; -import { mkdtempSync, readFileSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; - -const maxSide = "800"; - -function run(file: string, args: string[]): Promise { - return new Promise((resolve, reject) => - execFile(file, args, { timeout: 3000 }, (error) => (error ? reject(error) : resolve())), - ); -} - -// A picture of one window as a data URI, or undefined when macOS refuses (no Screen Recording). -// The file only lives while it is read: these are pictures of the user's windows. -export async function capturePreview(windowID: number): Promise { - const folder = mkdtempSync(join(tmpdir(), "defi-preview-")); - const file = join(folder, "window.jpg"); - try { - await run("/usr/sbin/screencapture", ["-x", "-o", "-l", String(windowID), "-t", "jpg", file]); - await run("/usr/bin/sips", ["-Z", maxSide, "-s", "formatOptions", "70", file]); - return `data:image/jpeg;base64,${readFileSync(file).toString("base64")}`; - } catch { - return undefined; - } finally { - rmSync(folder, { recursive: true, force: true }); - } -} diff --git a/contrib/raycast/src/strip-detail.tsx b/contrib/raycast/src/strip-detail.tsx index 892f5b40..45fa5c26 100644 --- a/contrib/raycast/src/strip-detail.tsx +++ b/contrib/raycast/src/strip-detail.tsx @@ -9,15 +9,13 @@ type Props = { dark: boolean; appIcon: (bundleID?: string) => Image.ImageLike | undefined; empty: string; - // A picture of the highlighted window, shown above its strip. - preview?: string; children?: React.ReactNode; }; const maxTagLength = 24; // The workspace's strip, then the caller's rows, then its columns named in order. -export function StripDetail({ windows, workspaceID, highlightID, dark, appIcon, empty, preview, children }: Props) { +export function StripDetail({ windows, workspaceID, highlightID, dark, appIcon, empty, children }: Props) { const columns = stripColumns(windows, workspaceID); const floating = windows.filter((window) => window.workspace?.id === workspaceID && window.floating); const onWorkspace = [...columns.flat(), ...floating]; @@ -38,9 +36,7 @@ export function StripDetail({ windows, workspaceID, highlightID, dark, appIcon, )); return ( {children} diff --git a/contrib/raycast/src/window-list.tsx b/contrib/raycast/src/window-list.tsx index 7a4d8f5e..f9097753 100644 --- a/contrib/raycast/src/window-list.tsx +++ b/contrib/raycast/src/window-list.tsx @@ -1,12 +1,11 @@ import { Action, ActionPanel, Color, environment, Icon, Keyboard, List } from "@raycast/api"; import { showFailureToast, useCachedState, useFrecencySorting } from "@raycast/utils"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { runAndClose } from "./defi"; import { useDesktop } from "./desktop"; import { ErrorView } from "./error-view"; import { workspaceLabel, type DefiWindow, type DefiWorkspace } from "./parse"; import { StripDetail } from "./strip-detail"; -import { capturePreview } from "./preview"; import { stripColumns } from "./strip"; import { workspaceAccessories, workspaceIcon } from "./workspace-icon"; @@ -87,14 +86,6 @@ export function WindowList({ initialSearch = "", workspaceID }: { initialSearch? // The focused window goes last: Enter on it would change nothing. const sorted = [...inScope.filter((window) => !window.focused), ...inScope.filter((window) => window.focused)]; const dark = environment.appearance === "dark"; - // Pictures of windows, taken one at a time for the selected row while the side pane is open. - const [selectedID, setSelectedID] = useState(null); - const [previews, setPreviews] = useState>({}); - useEffect(() => { - if (!showDetail || !selectedID || selectedID in previews) return; - setPreviews((taken) => ({ ...taken, [selectedID]: undefined })); - capturePreview(Number(selectedID)).then((preview) => setPreviews((taken) => ({ ...taken, [selectedID]: preview }))); - }, [showDetail, selectedID]); const position = (window: DefiWindow) => { if (!window.workspace || !window.column) return "Floating"; @@ -175,7 +166,6 @@ export function WindowList({ initialSearch = "", workspaceID }: { initialSearch? searchBarAccessory={scopeMenu} searchText={searchText} onSearchTextChange={setSearchText} - onSelectionChange={setSelectedID} filtering > {emptyView()} @@ -186,7 +176,6 @@ export function WindowList({ initialSearch = "", workspaceID }: { initialSearch? return ( {window.title ? : null} From 4769790e734d1501649c1ae66ce418d59f80a6a9 Mon Sep 17 00:00:00 2001 From: Quentin Eude Date: Wed, 11 Nov 2026 17:39:44 +0100 Subject: [PATCH 22/23] feat(contrib): lighter Search Workspaces rows Keep the side pane closed by default so rows keep their app icons, and show each workspace's window count or Empty as its subtitle. --- contrib/raycast/README.md | 4 ++-- contrib/raycast/src/search-workspaces.tsx | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/contrib/raycast/README.md b/contrib/raycast/README.md index 296b6a92..fc86b17f 100644 --- a/contrib/raycast/README.md +++ b/contrib/raycast/README.md @@ -21,8 +21,8 @@ that workspace for a moment, because Defi focuses the window first. | ⌘⇧F | Float or tile it | | ⌘⇧C / ⌘⇧I | Copy its title or ID | -**Search Workspaces** lists the workspaces of every display with their apps and -strip; search by name, number, or app. +**Search Workspaces** lists the workspaces of every display with their window +count and apps; search by name, number, or app. ⌘Y shows a workspace's columns. | Key | Action | | --- | --- | diff --git a/contrib/raycast/src/search-workspaces.tsx b/contrib/raycast/src/search-workspaces.tsx index 9fb6f574..b2d207bc 100644 --- a/contrib/raycast/src/search-workspaces.tsx +++ b/contrib/raycast/src/search-workspaces.tsx @@ -24,7 +24,7 @@ export default function Command(props: LaunchProps) { const desktop = useDesktop(); const { windows, workspaces, focused, displayCount, appIcon, appName } = desktop; const [searchText, setSearchText] = useState(props.fallbackText ?? ""); - const [showDetail, setShowDetail] = useCachedState("workspaces-strip", true); + const [showDetail, setShowDetail] = useCachedState("workspaces-detail", false); const dark = environment.appearance === "dark"; // Cached rows would look live while Defi is unreachable, so an error hides them. const shown = desktop.error ? [] : (workspaces ?? []); @@ -142,6 +142,11 @@ export default function Command(props: LaunchProps) { key={workspace.id} icon={workspaceIcon(workspace, workspace.active)} title={label} + subtitle={ + workspace.windowCount === 0 + ? "Empty" + : `${workspace.windowCount} window${workspace.windowCount === 1 ? "" : "s"}` + } keywords={[String(workspace.position), ...workspace.applications.map(appName)]} // Rows next to the side pane stay bare; the pane names every app. accessories={showDetail ? undefined : workspaceAccessories(workspace, appIcon, appName)} From 14e23f6dd5b1c4afe9b349d1833d65098b439929 Mon Sep 17 00:00:00 2001 From: Quentin Eude Date: Wed, 11 Nov 2026 17:48:47 +0100 Subject: [PATCH 23/23] chore(contrib): prepare the Raycast extension for the Store Store README and changelog placeholder, the standard publish scripts instead of the Tinycast one, a catalog test that runs only next to a Defi checkout, and update advice that points to the Store. --- contrib/raycast/CHANGELOG.md | 2 +- contrib/raycast/README.md | 42 ++++++++------------------ contrib/raycast/package.json | 5 +-- contrib/raycast/src/error-view.tsx | 2 +- contrib/raycast/test/commands.test.mts | 18 +++++++---- 5 files changed, 30 insertions(+), 39 deletions(-) diff --git a/contrib/raycast/CHANGELOG.md b/contrib/raycast/CHANGELOG.md index 37986a37..d9944ce7 100644 --- a/contrib/raycast/CHANGELOG.md +++ b/contrib/raycast/CHANGELOG.md @@ -1,6 +1,6 @@ # Defi Changelog -## [Initial Version] - 2026-11-11 +## [Initial Version] - {PR_MERGE_DATE} - Search Windows: focus any managed window, bring it to the current workspace, move it, float it, or copy its title or ID. - Search Workspaces: jump to a workspace on any display, or move the focused window or column there. diff --git a/contrib/raycast/README.md b/contrib/raycast/README.md index fc86b17f..32657497 100644 --- a/contrib/raycast/README.md +++ b/contrib/raycast/README.md @@ -1,10 +1,12 @@ -# Raycast extension +# Defi Reach [Defi](https://github.com/qeude/Defi), the scrolling-columns window -manager for macOS, from [Raycast](https://www.raycast.com) or -[Tinycast](https://github.com/abue-ammar/tinycast). The extension only runs the -`defi` command line tool inside Defi.app and makes no network calls, so window -titles stay on your Mac. +manager for macOS, from Raycast: switch windows, jump between workspaces, and +run any Defi command. The extension only runs the `defi` command line tool +inside Defi.app and makes no network calls, so window titles stay on your Mac. + +Requires [Defi.app](https://github.com/qeude/Defi/releases/latest) in +`/Applications` or `~/Applications`, or picked in the extension's preferences. ## Commands @@ -22,7 +24,7 @@ that workspace for a moment, because Defi focuses the window first. | ⌘⇧C / ⌘⇧I | Copy its title or ID | **Search Workspaces** lists the workspaces of every display with their window -count and apps; search by name, number, or app. ⌘Y shows a workspace's columns. +count and apps; search by name, number, or app. | Key | Action | | --- | --- | @@ -46,32 +48,14 @@ Type the same text after the command in the root search, as in [Commands](https://github.com/qeude/Defi/blob/main/CONFIGURATION.md#commands). In every list, ⌘Y shows or hides the side pane and ⌘R reloads. Shortcuts shown -are Defi's defaults, with `mod` as ⌃⌥; your own `[keys]` may differ. - -## Install - -You need Node.js 22.6 or later and Defi.app. From the Defi repository root: - -```sh -cd contrib/raycast -npm install && npm run dev -``` - -`npm run dev` adds the extension to Raycast in development mode. Give Search -Windows a hotkey or an alias such as `w` in its command settings (⌘K, then -Configure Command) to reach it in one keystroke. - -For Tinycast, run `npm run tinycast` instead: it builds straight into Tinycast's -extensions folder, and Tinycast picks up a new build the next time a command -starts. +are Defi's defaults, with `mod` as ⌃⌥; your own `[keys]` may differ. Give Search +Windows a hotkey or an alias such as `w` to reach it in one keystroke. ## Troubleshooting -- **Defi Is Not Installed**: the extension looks for Defi.app in - `/Applications`, then `~/Applications`. If it lives elsewhere, pick it in - **Defi App** in the extension's preferences. +- **Defi Is Not Installed**: install Defi, or pick Defi.app in **Defi App** in + the extension's preferences. - **Defi Is Not Running**: press Enter to launch it; the list fills in once Defi starts. - **Update Defi** or **Update This Extension**: Defi and the extension read - different versions of the JSON format. Update the older one; a newer - extension is rebuilt from this folder. + different versions of the JSON format. Update the older one. diff --git a/contrib/raycast/package.json b/contrib/raycast/package.json index 0bd4b6dd..08982c46 100644 --- a/contrib/raycast/package.json +++ b/contrib/raycast/package.json @@ -89,7 +89,8 @@ "dev": "ray develop", "fix-lint": "ray lint --fix", "lint": "ray lint", - "tinycast": "ray build -e dist -o \"$HOME/Library/Application Support/com.tinycast.app/extensions/defi\"", - "test": "node --experimental-strip-types --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --disable-warning=ExperimentalWarning --test 'test/*.test.mts'" + "test": "node --experimental-strip-types --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --disable-warning=ExperimentalWarning --test 'test/*.test.mts'", + "prepublishOnly": "echo \"\\n\\nIt seems like you are trying to publish the Raycast extension to npm.\\n\\nIf you did intend to publish it to npm, remove the \\`prepublishOnly\\` script and rerun \\`npm publish\\` again.\\nIf you wanted to publish it to the Raycast Store instead, use \\`npm run publish\\` instead.\\n\\n\" && exit 1", + "publish": "npx @raycast/api@latest publish" } } diff --git a/contrib/raycast/src/error-view.tsx b/contrib/raycast/src/error-view.tsx index 6a5f2b94..82ec7d06 100644 --- a/contrib/raycast/src/error-view.tsx +++ b/contrib/raycast/src/error-view.tsx @@ -21,7 +21,7 @@ export function describeError(error: Error) { title: error.received > error.supported ? "Update This Extension" : "Update Defi", description: error.received > error.supported - ? "Defi is newer than this extension. Rebuild the extension from the Defi repository." + ? "Defi is newer than this extension. Update the extension from the Raycast Store." : "This extension needs a newer Defi. Update Defi from its menu bar item or the releases page.", }; } diff --git a/contrib/raycast/test/commands.test.mts b/contrib/raycast/test/commands.test.mts index 1d369bcb..077a7715 100644 --- a/contrib/raycast/test/commands.test.mts +++ b/contrib/raycast/test/commands.test.mts @@ -1,14 +1,20 @@ import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; import { test } from "node:test"; import { describeCommand, prompts, sections } from "../src/commands.ts"; -// The catalog teaches commands and default shortcuts, so it must match the reference it copies. -const reference = readFileSync(new URL("../../../CONFIGURATION.md", import.meta.url), "utf8"); -const rows = reference.split("\n").filter((line) => line.startsWith("| `")); +// The catalog teaches commands and default shortcuts, so it must match the reference it copies: +// Defi's CONFIGURATION.md, from DEFI_CONFIGURATION or a Defi checkout around this folder. +const referencePath = process.env.DEFI_CONFIGURATION ?? new URL("../../../CONFIGURATION.md", import.meta.url).pathname; +const skip = existsSync(referencePath) ? false : "set DEFI_CONFIGURATION to Defi's CONFIGURATION.md"; +const rows = skip + ? [] + : readFileSync(referencePath, "utf8") + .split("\n") + .filter((line) => line.startsWith("| `")); const entries = sections.flatMap(([, list]) => list); -test("every catalog command is a documented command with a description", () => { +test("every catalog command is a documented command with a description", { skip }, () => { for (const { command } of entries) { const verb = command.split(" ")[0]; assert.ok( @@ -23,7 +29,7 @@ test("every catalog command is a documented command with a description", () => { assert.equal(new Set(texts).size, texts.length, "two rows share a description"); }); -test("every default shortcut matches the documented one", () => { +test("every default shortcut matches the documented one", { skip }, () => { for (const { command, shortcut } of entries.filter((entry) => entry.shortcut)) { const [verb, direction = ""] = command.split(" "); // A command can have one row per group of arguments, such as left|right and first|last.