diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..751564f
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,4 @@
+{
+ "plugins": ["gjs"],
+ "extends": ["plugin:gjs/application"]
+}
\ No newline at end of file
diff --git a/ambient.d.ts b/ambient.d.ts
new file mode 100644
index 0000000..60c748b
--- /dev/null
+++ b/ambient.d.ts
@@ -0,0 +1,4 @@
+import "@girs/gjs";
+import "@girs/gdk-3.0";
+import "@girs/gjs/dom";
+import "@girs/gtk-3.0";
diff --git a/app.ts b/app.ts
index 0d0f4b9..9ebd4ed 100644
--- a/app.ts
+++ b/app.ts
@@ -1,11 +1,16 @@
-import { App } from "astal/gtk3"
-import style from "./style.css"
-import Bar from "./widget/Bar"
+import { App } from "astal/gtk3";
+import style from "./style.css";
+import Bar from "./widget/Bar";
+import "@/globals";
+import { Sway } from "@services/sway/ipc";
+import { Tree } from "@services/sway/workspaces";
+
+new Tree("/home/ezri/.config/sway/workspaces.json");
App.start({
- css: style,
- main() {
- Bar(0)
- // Bar(1) // initialize other monitors
- },
-})
+ css: style,
+ main() {
+ Bar(0);
+ // Bar(1) // initialize other monitors
+ },
+});
diff --git a/dbus/dbus.ts b/dbus/dbus.ts
new file mode 100644
index 0000000..e69de29
diff --git a/dbus/dev.ezri.voidshell.workspaces.Context.xml b/dbus/dev.ezri.voidshell.workspaces.Context.xml
new file mode 100644
index 0000000..9fb5240
--- /dev/null
+++ b/dbus/dev.ezri.voidshell.workspaces.Context.xml
@@ -0,0 +1,3 @@
+
diff --git a/dbus/dev.ezri.voidshell.workspaces.Group.xml b/dbus/dev.ezri.voidshell.workspaces.Group.xml
new file mode 100644
index 0000000..e9dd263
--- /dev/null
+++ b/dbus/dev.ezri.voidshell.workspaces.Group.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/dbus/dev.ezri.voidshell.workspaces.Workspace.xml b/dbus/dev.ezri.voidshell.workspaces.Workspace.xml
new file mode 100644
index 0000000..b164f95
--- /dev/null
+++ b/dbus/dev.ezri.voidshell.workspaces.Workspace.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..fb121ae
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1406 @@
+{
+ "name": "VoidShell",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "VoidShell",
+ "version": "0.0.0",
+ "license": "All right reserved",
+ "dependencies": {
+ "@girs/gdk-3.0": "^3.24.43-4.0.0-beta.16",
+ "@girs/gjs": "^4.0.0-beta.16"
+ },
+ "devDependencies": {
+ "eslint": "^9.14.0",
+ "eslint-plugin-jsdoc": "^50.4.3",
+ "typescript": "^5.6.3"
+ }
+ },
+ "node_modules/@es-joy/jsdoccomment": {
+ "version": "0.49.0",
+ "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.49.0.tgz",
+ "integrity": "sha512-xjZTSFgECpb9Ohuk5yMX5RhUEbfeQcuOp8IF60e+wyzWEF0M5xeSgqsfLtvPEX8BIyOX9saZqzuGPmZ8oWc+5Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "comment-parser": "1.4.1",
+ "esquery": "^1.6.0",
+ "jsdoc-type-pratt-parser": "~4.1.0"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz",
+ "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==",
+ "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/eslint-utils/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-community/regexpp": {
+ "version": "4.12.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
+ "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz",
+ "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.4",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz",
+ "integrity": "sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz",
+ "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.14.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.14.0.tgz",
+ "integrity": "sha512-pFoEtFWCPyDOl+C6Ift+wC7Ro89otjigCf5vcuWqWgqNSQbRrpjSvdeE6ofLz4dHmyxD5f7gIdGT4+p36L6Twg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz",
+ "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.2.tgz",
+ "integrity": "sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@girs/cairo-1.0": {
+ "version": "1.0.0-4.0.0-beta.16",
+ "resolved": "https://registry.npmjs.org/@girs/cairo-1.0/-/cairo-1.0-1.0.0-4.0.0-beta.16.tgz",
+ "integrity": "sha512-nIrOvL73lhCHB/4n4ro7Ud4XaR0DrUg+MvCN/B42L2eCS8YIZ0MHZ+Fma0shifDQyv8uoopHfE5I7X/v7VOYFg==",
+ "license": "MIT",
+ "dependencies": {
+ "@girs/gjs": "^4.0.0-beta.16",
+ "@girs/glib-2.0": "^2.82.0-4.0.0-beta.16",
+ "@girs/gobject-2.0": "^2.82.0-4.0.0-beta.16"
+ }
+ },
+ "node_modules/@girs/freetype2-2.0": {
+ "version": "2.0.0-4.0.0-beta.16",
+ "resolved": "https://registry.npmjs.org/@girs/freetype2-2.0/-/freetype2-2.0-2.0.0-4.0.0-beta.16.tgz",
+ "integrity": "sha512-djReh9OOKW5vG8t94ie4mQmsUWS4qgLWLMBIFg/KKcCrGHYgwkcSafLnnn/5HyRgdz6qxoFm6N3C+AJEcYYz6A==",
+ "license": "MIT",
+ "dependencies": {
+ "@girs/gjs": "^4.0.0-beta.16",
+ "@girs/gobject-2.0": "^2.82.0-4.0.0-beta.16"
+ }
+ },
+ "node_modules/@girs/gdk-3.0": {
+ "version": "3.24.43-4.0.0-beta.16",
+ "resolved": "https://registry.npmjs.org/@girs/gdk-3.0/-/gdk-3.0-3.24.43-4.0.0-beta.16.tgz",
+ "integrity": "sha512-AKFbDjRrrNcKlbv57tFfcgCLIimn2mYJypqK7HzK5JIYvm9dogBnCTC9BpIMyFF/RcClCOxWgn7WnaAVzOSRrw==",
+ "license": "MIT",
+ "dependencies": {
+ "@girs/cairo-1.0": "^1.0.0-4.0.0-beta.16",
+ "@girs/freetype2-2.0": "^2.0.0-4.0.0-beta.16",
+ "@girs/gdkpixbuf-2.0": "^2.0.0-4.0.0-beta.16",
+ "@girs/gio-2.0": "^2.82.0-4.0.0-beta.16",
+ "@girs/gjs": "^4.0.0-beta.16",
+ "@girs/glib-2.0": "^2.82.0-4.0.0-beta.16",
+ "@girs/gmodule-2.0": "^2.0.0-4.0.0-beta.16",
+ "@girs/gobject-2.0": "^2.82.0-4.0.0-beta.16",
+ "@girs/harfbuzz-0.0": "^9.0.0-4.0.0-beta.16",
+ "@girs/pango-1.0": "^1.54.0-4.0.0-beta.16"
+ }
+ },
+ "node_modules/@girs/gdkpixbuf-2.0": {
+ "version": "2.0.0-4.0.0-beta.16",
+ "resolved": "https://registry.npmjs.org/@girs/gdkpixbuf-2.0/-/gdkpixbuf-2.0-2.0.0-4.0.0-beta.16.tgz",
+ "integrity": "sha512-d4v7fGCuegpf5sscnMsOCjeuQgJjUJ4+Gozr+wFL30Are944V1FYWrdWP1GgIaNnBeWXY43D58YFQhsfFi+bWg==",
+ "license": "MIT",
+ "dependencies": {
+ "@girs/gio-2.0": "^2.82.0-4.0.0-beta.16",
+ "@girs/gjs": "^4.0.0-beta.16",
+ "@girs/glib-2.0": "^2.82.0-4.0.0-beta.16",
+ "@girs/gmodule-2.0": "^2.0.0-4.0.0-beta.16",
+ "@girs/gobject-2.0": "^2.82.0-4.0.0-beta.16"
+ }
+ },
+ "node_modules/@girs/gio-2.0": {
+ "version": "2.82.0-4.0.0-beta.16",
+ "resolved": "https://registry.npmjs.org/@girs/gio-2.0/-/gio-2.0-2.82.0-4.0.0-beta.16.tgz",
+ "integrity": "sha512-5JW6qgyzh3OW7dEihP+h2ZtXWw7kDXi/ra63ZscnYqUIuoebqaSB66HFzd3AMyDYYQ7j8IGZdJCog+6kKwnk8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@girs/gjs": "^4.0.0-beta.16",
+ "@girs/glib-2.0": "^2.82.0-4.0.0-beta.16",
+ "@girs/gmodule-2.0": "^2.0.0-4.0.0-beta.16",
+ "@girs/gobject-2.0": "^2.82.0-4.0.0-beta.16"
+ }
+ },
+ "node_modules/@girs/gjs": {
+ "version": "4.0.0-beta.16",
+ "resolved": "https://registry.npmjs.org/@girs/gjs/-/gjs-4.0.0-beta.16.tgz",
+ "integrity": "sha512-UjWj6GXXCqJ+z8DdzQ2XYxl1IqzgcIOIs6BTcEsXQfaZx9704GNpd9PRcqOKj7hVe2VGqO5VdIbk80u9Bo+lUQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@girs/cairo-1.0": "^1.0.0-4.0.0-beta.16",
+ "@girs/gio-2.0": "^2.82.0-4.0.0-beta.16",
+ "@girs/glib-2.0": "^2.82.0-4.0.0-beta.16",
+ "@girs/gobject-2.0": "^2.82.0-4.0.0-beta.16"
+ }
+ },
+ "node_modules/@girs/glib-2.0": {
+ "version": "2.82.0-4.0.0-beta.16",
+ "resolved": "https://registry.npmjs.org/@girs/glib-2.0/-/glib-2.0-2.82.0-4.0.0-beta.16.tgz",
+ "integrity": "sha512-YZjsythyPhClrq0uEGq8rBJ8ER0iBPf3alr1kkSOt28tDG4tn0Rv2kudHEKsFenlFjzhYVOMrJSjVvsxtcqRUQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@girs/gjs": "^4.0.0-beta.16",
+ "@girs/gobject-2.0": "^2.82.0-4.0.0-beta.16"
+ }
+ },
+ "node_modules/@girs/gmodule-2.0": {
+ "version": "2.0.0-4.0.0-beta.16",
+ "resolved": "https://registry.npmjs.org/@girs/gmodule-2.0/-/gmodule-2.0-2.0.0-4.0.0-beta.16.tgz",
+ "integrity": "sha512-jhl7j0JOtttJnOFQ4OypPjGP5DL8AlEiSsd8YKpXI3CEC7tUFZXHq+/mymFeZ6m4GFLEdC4j0efl9roPTSEYnA==",
+ "license": "MIT",
+ "dependencies": {
+ "@girs/gjs": "^4.0.0-beta.16",
+ "@girs/glib-2.0": "^2.82.0-4.0.0-beta.16",
+ "@girs/gobject-2.0": "^2.82.0-4.0.0-beta.16"
+ }
+ },
+ "node_modules/@girs/gobject-2.0": {
+ "version": "2.82.0-4.0.0-beta.16",
+ "resolved": "https://registry.npmjs.org/@girs/gobject-2.0/-/gobject-2.0-2.82.0-4.0.0-beta.16.tgz",
+ "integrity": "sha512-ziBHUMSVuoHlzhZrMvuJDSZPOR4HzRW+nk35aI3QW9TpHtIYTk6vWvY0EG0+ylzJlltT9C/kzoVvspyMulBZ5g==",
+ "license": "MIT",
+ "dependencies": {
+ "@girs/gjs": "^4.0.0-beta.16",
+ "@girs/glib-2.0": "^2.82.0-4.0.0-beta.16"
+ }
+ },
+ "node_modules/@girs/harfbuzz-0.0": {
+ "version": "9.0.0-4.0.0-beta.16",
+ "resolved": "https://registry.npmjs.org/@girs/harfbuzz-0.0/-/harfbuzz-0.0-9.0.0-4.0.0-beta.16.tgz",
+ "integrity": "sha512-ONBRGCdBj668eComG25kOZVDU6HqRZWHIA42YYlB0JxF6Dgtu52sLiDoGXh00gkpATqnFbHfkDJhiL9EowGgIQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@girs/freetype2-2.0": "^2.0.0-4.0.0-beta.16",
+ "@girs/gjs": "^4.0.0-beta.16",
+ "@girs/glib-2.0": "^2.82.0-4.0.0-beta.16",
+ "@girs/gobject-2.0": "^2.82.0-4.0.0-beta.16"
+ }
+ },
+ "node_modules/@girs/pango-1.0": {
+ "version": "1.54.0-4.0.0-beta.16",
+ "resolved": "https://registry.npmjs.org/@girs/pango-1.0/-/pango-1.0-1.54.0-4.0.0-beta.16.tgz",
+ "integrity": "sha512-EY1/70Q1eZ8B3DlkB+QKgK4BLRM2B0sl8tZiHxy99H72Yl+DW2yqyXrG5mx5TxVfphd2hB5x3+vREej15XSgRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@girs/cairo-1.0": "^1.0.0-4.0.0-beta.16",
+ "@girs/freetype2-2.0": "^2.0.0-4.0.0-beta.16",
+ "@girs/gio-2.0": "^2.82.0-4.0.0-beta.16",
+ "@girs/gjs": "^4.0.0-beta.16",
+ "@girs/glib-2.0": "^2.82.0-4.0.0-beta.16",
+ "@girs/gmodule-2.0": "^2.0.0-4.0.0-beta.16",
+ "@girs/gobject-2.0": "^2.82.0-4.0.0-beta.16",
+ "@girs/harfbuzz-0.0": "^9.0.0-4.0.0-beta.16"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.6",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz",
+ "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
+ "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "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.0",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.0.tgz",
+ "integrity": "sha512-xnRgu9DxZbkWak/te3fcytNyp8MTbuiZIaueg2rgEvBuN55n04nwLYLU9TX/VVlusc9L2ZNXi99nUFNkHXtr5g==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@pkgr/core": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz",
+ "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unts"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
+ "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/acorn": {
+ "version": "8.14.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
+ "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
+ "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.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "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-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/are-docs-informative": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz",
+ "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/comment-parser": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz",
+ "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12.0.0"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dev": true,
+ "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/es-module-lexer": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz",
+ "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.14.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.14.0.tgz",
+ "integrity": "sha512-c2FHsVBr87lnUtjP4Yhvk4yEhKrQavGafRA/Se1ouse8PfbfC/Qh9Mxa00yWsZRlqeUB9raXip0aiiUZkgnr9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.18.0",
+ "@eslint/core": "^0.7.0",
+ "@eslint/eslintrc": "^3.1.0",
+ "@eslint/js": "9.14.0",
+ "@eslint/plugin-kit": "^0.2.0",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.0",
+ "@types/estree": "^1.0.6",
+ "@types/json-schema": "^7.0.15",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.2.0",
+ "eslint-visitor-keys": "^4.2.0",
+ "espree": "^10.3.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "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",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3",
+ "text-table": "^0.2.0"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-jsdoc": {
+ "version": "50.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.4.3.tgz",
+ "integrity": "sha512-uWtwFxGRv6B8sU63HZM5dAGDhgsatb+LONwmILZJhdRALLOkCX2HFZhdL/Kw2ls8SQMAVEfK+LmnEfxInRN8HA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@es-joy/jsdoccomment": "~0.49.0",
+ "are-docs-informative": "^0.0.2",
+ "comment-parser": "1.4.1",
+ "debug": "^4.3.6",
+ "escape-string-regexp": "^4.0.0",
+ "espree": "^10.1.0",
+ "esquery": "^1.6.0",
+ "parse-imports": "^2.1.1",
+ "semver": "^7.6.3",
+ "spdx-expression-parse": "^4.0.0",
+ "synckit": "^0.9.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz",
+ "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
+ "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.3.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
+ "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.14.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+ "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/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": "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/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "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": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
+ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "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": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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/import-fresh": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+ "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "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/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-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/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsdoc-type-pratt-parser": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz",
+ "integrity": "sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.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/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/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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/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/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/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-imports": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/parse-imports/-/parse-imports-2.2.1.tgz",
+ "integrity": "sha512-OL/zLggRp8mFhKL0rNORUTR4yBYujK/uU+xZL+/0Rgm2QE4nLO9v8PzEweSJEbMGKmDRjJE4R3IMJlL2di4JeQ==",
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "dependencies": {
+ "es-module-lexer": "^1.5.3",
+ "slashes": "^3.0.12"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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/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/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.6.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
+ "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/slashes": {
+ "version": "3.0.12",
+ "resolved": "https://registry.npmjs.org/slashes/-/slashes-3.0.12.tgz",
+ "integrity": "sha512-Q9VME8WyGkc7pJf6QEkj3wE+2CnvZMI+XJhwdTPR8Z/kWQRXi7boAWLDibRPyHRTUTPx5FaU7MsyrjI3yLB4HA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/spdx-exceptions": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
+ "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==",
+ "dev": true,
+ "license": "CC-BY-3.0"
+ },
+ "node_modules/spdx-expression-parse": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz",
+ "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-license-ids": {
+ "version": "3.0.20",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz",
+ "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/synckit": {
+ "version": "0.9.2",
+ "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz",
+ "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@pkgr/core": "^0.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unts"
+ }
+ },
+ "node_modules/text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "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/typescript": {
+ "version": "5.6.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
+ "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "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==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "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/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"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..c69d011
--- /dev/null
+++ b/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "VoidShell",
+ "version": "0.0.0",
+ "description": "Custom GTK3 desktop shell meant for Sway sessions",
+ "type": "module",
+ "private": true,
+ "repository": {
+ "type": "git",
+ "url": "git+https://git.ezri.dev/ezri/VoidShell.git"
+ },
+ "author": "Ezri Brimhall ",
+ "license": "All right reserved",
+ "devDependencies": {
+ "eslint": "^9.14.0",
+ "eslint-plugin-jsdoc": "^50.4.3",
+ "typescript": "^5.6.3"
+ },
+ "dependencies": {
+ "@girs/gdk-3.0": "^3.24.43-4.0.0-beta.16",
+ "@girs/gjs": "^4.0.0-beta.16"
+ }
+}
diff --git a/widget/services/System.ts b/services/System.ts
similarity index 93%
rename from widget/services/System.ts
rename to services/System.ts
index b03a3ce..3bd1a32 100644
--- a/widget/services/System.ts
+++ b/services/System.ts
@@ -12,7 +12,7 @@ export class System {
.split("\n")
.map((line) => line.trim())
.map((line) => /^PRETTY_HOSTNAME="?([^"]*)"?$/.exec(line))
- .filter((line) => line)
+ .filter((line) => line !== null)
.map((line) => line[1]);
if (lines.length > 0) {
this.#name.set(lines[0]);
@@ -38,7 +38,7 @@ export class System {
return System._instance;
}
- private #name: Variable = Variable("");
+ #name: Variable = Variable("");
/**
* The "pretty" hostname of this computer, as a bound value.
diff --git a/services/dbus.ts b/services/dbus.ts
new file mode 100644
index 0000000..44bf9c5
--- /dev/null
+++ b/services/dbus.ts
@@ -0,0 +1,42 @@
+import Gio from "gi://Gio";
+import GObject, { register } from "astal/gobject";
+import { getDbusXml } from "@/utils";
+
+let dbusConnectionResolve: ((connection: any) => void) | null = null;
+export const Connection: Promise = new Promise(
+ (resolve) => (dbusConnectionResolve = resolve),
+);
+
+const ownerId = Gio.bus_own_name(
+ Gio.BusType.SESSION,
+ "dev.ezri.VoidShell",
+ Gio.BusNameOwnerFlags.NONE,
+ (connection) => {
+ dbusConnectionResolve?.(connection);
+ },
+ () => {},
+ () => {},
+);
+
+@register()
+export class DBusObject extends GObject.Object {
+ protected dbusObj: Gio.DBusExportedObject | null = null;
+ #objectPath: string;
+
+ constructor(iface: string, objectPath: string) {
+ super();
+ this.#objectPath = objectPath;
+ getDbusXml(iface).then(async (xml) => {
+ try {
+ this.dbusObj = Gio.DBusExportedObject.wrapJSObject(xml, this);
+ this.dbusObj.export(await Connection, objectPath);
+ } catch (e) {
+ console.error(`Error exporting to D-Bus: ${e}`);
+ }
+ });
+ }
+
+ get objectPath(): string {
+ return this.#objectPath;
+ }
+}
diff --git a/widget/services/sway/definitions.ts b/services/sway/definitions.ts
similarity index 98%
rename from widget/services/sway/definitions.ts
rename to services/sway/definitions.ts
index afc3c12..48b2838 100644
--- a/widget/services/sway/definitions.ts
+++ b/services/sway/definitions.ts
@@ -163,7 +163,8 @@ export interface Context {
* As such, this value can be used to give a context a slight advantage in the case of a tie, boost a more general
* context over a more specific one, or remove it from consideration entirely.
*
- * This value is not considered when requesting a context by name.
+ * This value is not considered when requesting a context by name, allowing for contexts which cannot be automatically
+ * selected to be manually selected.
*/
priority?: number;
}
diff --git a/services/sway/exceptions.ts b/services/sway/exceptions.ts
new file mode 100644
index 0000000..4a6908d
--- /dev/null
+++ b/services/sway/exceptions.ts
@@ -0,0 +1,10 @@
+import type { Context } from "./workspaces";
+
+export class InvalidContextError extends Error {
+ constructor(
+ public readonly context: Context,
+ reason: string,
+ ) {
+ super(reason);
+ }
+}
diff --git a/services/sway/ipc.ts b/services/sway/ipc.ts
new file mode 100644
index 0000000..a67a50e
--- /dev/null
+++ b/services/sway/ipc.ts
@@ -0,0 +1,349 @@
+import GObject, { register, property, signal } from "astal/gobject";
+import {
+ WorkspaceResponse,
+ OutputResponse,
+ SwayMessage,
+ SwayEvent,
+ WorkspaceEvent,
+ OutputEvent,
+ TreeNode,
+ CommandResponse,
+} from "./types";
+import Gio from "gi://Gio";
+import GLib from "gi://GLib";
+import * as utils from "@/utils";
+import { Mutex } from "@/synchronization";
+
+@register({
+ GTypeName: "Sway",
+})
+export class Sway extends GObject.Object {
+ #commandSocket: Gio.SocketConnection | null = null;
+ #subscriptionSocket: Gio.SocketConnection | null = null;
+
+ #mode: string = "";
+
+ @signal()
+ /**
+ * Emitted when the connection to sway is established
+ */
+ declare connected: () => void;
+
+ @signal()
+ /**
+ * Emitted when the connection to sway is lost
+ */
+ declare disconnected: () => void;
+
+ // TODO: figure out how to give these signals a proper type. For now, Object is /fine/.
+
+ @signal(Object)
+ /**
+ * Emitted when we receive a subscription event
+ */
+ declare subscribed: (events: SwayEvent[]) => void;
+
+ @signal(Object)
+ /**
+ * Emitted when we receive a workspace event
+ */
+ declare workspace: (event: WorkspaceEvent) => void;
+
+ @signal(Object)
+ /**
+ * Emitted when we receive an output event
+ */
+ declare output: (event: OutputEvent) => void;
+
+ @property(String)
+ /**
+ * The current binding mode of sway
+ */
+ get mode(): string {
+ return this.#mode;
+ }
+
+ @property(Boolean)
+ get isConnected(): boolean {
+ return this.#connected;
+ }
+
+ #connected = false;
+
+ /**
+ * Run a command on sway
+ */
+ public async command(command: string): Promise {
+ return JSON.parse(
+ (await this.#sendRaw(
+ this.#commandSocket,
+ SwayMessage.RUN_COMMAND,
+ command,
+ )) as string,
+ );
+ }
+
+ /**
+ * Get the current sway workspaces
+ */
+ public async getWorkspaces(): Promise {
+ return JSON.parse(
+ (await this.#sendRaw(
+ this.#commandSocket,
+ SwayMessage.GET_WORKSPACES,
+ )) as string,
+ );
+ }
+
+ /**
+ * Get the current sway outputs
+ */
+ public async getOutputs(): Promise {
+ return JSON.parse(
+ (await this.#sendRaw(
+ this.#commandSocket,
+ SwayMessage.GET_OUTPUTS,
+ )) as string,
+ );
+ }
+
+ /**
+ * Get the current sway tree
+ */
+ public async getTree(): Promise {
+ return JSON.parse(
+ (await this.#sendRaw(
+ this.#commandSocket,
+ SwayMessage.GET_TREE,
+ )) as string,
+ );
+ }
+
+ constructor() {
+ super();
+ this.#connect();
+ }
+
+ static _instance: Sway;
+
+ /**
+ * Get the default instance of Sway
+ */
+ static get instance() {
+ if (!Sway._instance) {
+ Sway._instance = new Sway();
+ }
+ return Sway._instance;
+ }
+
+ async #createConnection(address: string): Promise {
+ console.log(`Connecting to sway socket ${address}`);
+ const client = new Gio.SocketClient();
+ return new Promise((resolve, reject) => {
+ const socket_addr = new Gio.UnixSocketAddress({ path: address });
+ client.connect_async(socket_addr, null, (client, result) => {
+ try {
+ // Type assertion is safe, if it fails we reject the promise in the catch block
+ const connection = client!.connect_finish(result);
+ resolve(connection);
+ } catch (e) {
+ console.error(`Failed to connect to socket.`, e);
+ reject(e);
+ }
+ });
+ });
+ }
+
+ async #getMode(): Promise {
+ const result = JSON.parse(
+ (await this.#sendRaw(
+ this.#commandSocket,
+ SwayMessage.GET_BINDING_STATE,
+ )) as string,
+ );
+ return result.name;
+ }
+
+ async #connect() {
+ console.log("Connecting to sway");
+ const address = GLib.getenv("SWAYSOCK");
+ if (!address) {
+ console.error("SWAYSOCK not set");
+ throw new Error("SWAYSOCK not set");
+ }
+
+ this.#commandSocket = await this.#createConnection(address);
+ this.#subscriptionSocket = await this.#createConnection(address);
+ console.log("Connected to sway");
+
+ // Start listening for subscriptions
+ this.#startSubscriberListen();
+
+ // Get the current mode
+ this.#mode = await this.#getMode();
+ this.notify("mode");
+
+ this.connected();
+ this.#connected = true;
+ this.notify("is-connected");
+ }
+
+ static readonly MAGIC = "i3-ipc";
+ static readonly HEADER_SIZE = Sway.MAGIC.length + 8;
+ static readonly CHUNK_SIZE = 4096;
+
+ async #startSubscriberListen() {
+ if (!this.#subscriptionSocket) {
+ console.error("Not connected");
+ throw new Error("Not connected");
+ }
+ await this.#subscribe(
+ SwayEvent.OUTPUT,
+ SwayEvent.WORKSPACE,
+ SwayEvent.MODE,
+ SwayEvent.SHUTDOWN,
+ );
+ const read = async () => {
+ while (true) {
+ try {
+ const [event, payload] = await this.#readRaw(
+ this.#subscriptionSocket!,
+ );
+ switch (event) {
+ case SwayEvent.OUTPUT:
+ this.output(JSON.parse(payload) as OutputEvent);
+ break;
+
+ case SwayEvent.WORKSPACE:
+ this.workspace(JSON.parse(payload) as WorkspaceEvent);
+ break;
+ case SwayEvent.MODE:
+ this.#mode = JSON.parse(payload).change;
+ this.notify("mode");
+ break;
+ case SwayEvent.SHUTDOWN:
+ this.disconnected();
+ break;
+ default:
+ console.warn("Unhandled event", event);
+ break;
+ }
+ } catch (e) {
+ console.error("Failed to read event", e);
+ }
+ }
+ };
+ read();
+ }
+
+ async #readHeader(
+ stream: Gio.InputStream,
+ ): Promise<{ length: number; type: SwayMessage | SwayEvent }> {
+ // We read one byte at a time until we have constructed a full magic string, then we read the rest of the header.
+ // This allows us to handle partial reads and corrupted data gracefully.
+ let idx = 0;
+ while (idx < Sway.MAGIC.length) {
+ const buffer = await utils.readFromStreamRaw(stream, 1);
+ const byte = buffer[0];
+ if (byte !== Sway.MAGIC.charCodeAt(idx)) {
+ // Reset if we don't match the magic string
+ idx = 0;
+ } else {
+ // Otherwise, keep going
+ idx++;
+ }
+ }
+ const header = new DataView((await utils.readFromStream(stream, 8)).buffer);
+ const length = header.getUint32(0, true);
+ const type = header.getUint32(4, true);
+ return { length, type };
+ }
+
+ async #readRaw(
+ socket: Gio.SocketConnection,
+ ): Promise<[SwayMessage | SwayEvent, string]> {
+ const inputStream = socket.input_stream;
+
+ const { length, type } = await this.#readHeader(inputStream);
+ const payloadBuf = await utils.readFromStream(inputStream, length);
+ const payload = new TextDecoder().decode(payloadBuf);
+
+ return [type as SwayMessage | SwayEvent, payload];
+ }
+
+ private messageMutex = new Mutex();
+
+ async #sendRaw(
+ socket: Gio.SocketConnection | null,
+ type: SwayMessage,
+ payload: string = "",
+ waitForResponse: boolean = true,
+ ): Promise {
+ return this.messageMutex.with(async () => {
+ if (!socket || !socket.is_connected()) {
+ console.error("Not connected");
+ throw new Error("Not connected");
+ }
+ if (socket === this.#commandSocket) {
+ if (type === SwayMessage.SUBSCRIBE) {
+ console.error("Cannot subscribe on command socket");
+ throw new Error("Cannot subscribe on command socket");
+ }
+ }
+ // Construct the message
+ const buffer = new ArrayBuffer(Sway.HEADER_SIZE + payload.length);
+ const magicView = new Uint8Array(buffer, 0, Sway.MAGIC.length);
+ const lengthView = new DataView(buffer, Sway.MAGIC.length, 4);
+ const typeView = new DataView(buffer, Sway.MAGIC.length + 4, 4);
+ const payloadView = new Uint8Array(buffer, Sway.HEADER_SIZE);
+
+ magicView.set(Sway.MAGIC.split("").map((c) => c.charCodeAt(0)));
+ lengthView.setUint32(0, payload.length, true);
+ typeView.setUint32(0, type, true);
+ payloadView.set(payload.split("").map((c) => c.charCodeAt(0)));
+
+ const outputStream = socket.output_stream;
+
+ // Send the message
+ try {
+ await utils.writeToStream(outputStream, buffer);
+ } catch (e) {
+ console.error("Failed to write to stream...", e);
+ throw e;
+ }
+ if (!waitForResponse) {
+ return null;
+ }
+
+ // Read the response
+ const [resultType, result] = await this.#readRaw(socket);
+ if (resultType !== type) {
+ throw new Error(`Unexpected response type: ${resultType}`);
+ }
+ return result;
+ });
+ }
+
+ async #subscribe(...events: SwayEvent[]) {
+ if (!this.#subscriptionSocket) {
+ console.error("Not connected");
+ throw new Error("Not connected");
+ }
+ const eventNames = events.map((e) => SwayEvent[e].toLowerCase());
+ const payload = JSON.stringify(eventNames);
+ console.log("Subscribing to events:", payload);
+ try {
+ await this.#sendRaw(
+ this.#subscriptionSocket,
+ SwayMessage.SUBSCRIBE,
+ payload,
+ );
+
+ console.log(`Subscribed to events: ${payload}`);
+ this.subscribed(events);
+ } catch (e) {
+ console.error(`Failed to subscribe:`, e);
+ throw e;
+ }
+ }
+}
diff --git a/services/sway/service.ts b/services/sway/service.ts
new file mode 100644
index 0000000..3756200
--- /dev/null
+++ b/services/sway/service.ts
@@ -0,0 +1,26 @@
+import * as Astal from "astal";
+import GObject, { register, property, signal } from "astal/gobject";
+import { Sway } from "./ipc";
+import * as types from "./types";
+
+@register({
+ GTypeName: "SwayService",
+})
+export class SwayService extends GObject.Object {
+ static _instance: SwayService;
+
+ constructor() {
+ super();
+ // Initialize the IPC and register event listeners
+ Sway.instance;
+ }
+
+ public static get instance(): SwayService {
+ if (!SwayService._instance) {
+ SwayService._instance = new SwayService();
+ }
+ return SwayService._instance;
+ }
+
+ #onWorkspaceEvent(event: types.WorkspaceEvent) {}
+}
diff --git a/widget/services/sway/types.ts b/services/sway/types.ts
similarity index 98%
rename from widget/services/sway/types.ts
rename to services/sway/types.ts
index 8e6cf5c..9258815 100644
--- a/widget/services/sway/types.ts
+++ b/services/sway/types.ts
@@ -252,7 +252,7 @@ export interface XWaylandWindowProperties {
*/
export interface TreeNode {
id: number;
- name: number;
+ name: string;
type: NodeType;
border: NodeBorder;
current_border_width: number;
@@ -351,8 +351,8 @@ export enum WorkspaceEventChange {
export interface WorkspaceEvent {
change: WorkspaceEventChange;
- current: WorkspaceResponse;
- old: WorkspaceResponse | null;
+ current: TreeNode;
+ old: TreeNode | null;
}
export interface OutputEvent {
diff --git a/services/sway/workspaces.ts b/services/sway/workspaces.ts
new file mode 100644
index 0000000..4699ed1
--- /dev/null
+++ b/services/sway/workspaces.ts
@@ -0,0 +1,807 @@
+import GObject, { register, property, signal } from "astal/gobject";
+import { readFileAsync, monitorFile } from "astal/file";
+import * as types from "./types";
+import * as defs from "./definitions";
+import Gdk from "gi://Gdk";
+import Gio from "gi://Gio";
+import GLib from "gi://GLib";
+import { Sway } from "./ipc";
+import { InvalidContextError } from "./exceptions";
+import { dereferenceSymbolicLink, getDbusXml } from "@/utils";
+import { Connection, DBusObject } from "@services/dbus";
+
+@register({
+ GTypeName: "SwayWorkspace",
+})
+export class Workspace extends DBusObject {
+ #active: boolean = false;
+ #focused: boolean = false;
+ #visible: boolean = false;
+ #urgent: boolean = false;
+
+ groups: Set = new Set();
+ #currentGroup: Group | null = null;
+ #currentOutput: string | null = null;
+
+ constructor(public readonly definition: defs.Workspace) {
+ super(
+ "dev.ezri.voidshell.workspaces.Workspace",
+ `/dev/ezri/VoidShell/workspaces/workspace/${definition.index}`,
+ );
+ }
+
+ /**
+ * Create an ephemeral workspace from a sync operation that is not defined in the system.
+ */
+ public static fromSync(update: types.WorkspaceResponse) {
+ const [name, index] = (() => {
+ if (parseInt(update.name).toString() === update.name) {
+ return [`ephemeral: ${update.name}`, parseInt(update.name)];
+ } else {
+ return [update.name, update.num];
+ }
+ })();
+ const definition: defs.Workspace = {
+ index,
+ name,
+ exec: "console",
+ program_name: "console",
+ };
+ const workspace = new this(definition);
+ workspace.sync(update);
+ return workspace;
+ }
+
+ @property(String)
+ get objectPath() {
+ return `/dev/ezri/VoidShell/workspaces/workspace/${this.definition.index}`;
+ }
+
+ @property(Boolean)
+ get active() {
+ return this.#active;
+ }
+
+ set active(value: boolean) {
+ if (this.#active === value) {
+ return;
+ }
+ this.#active = value;
+ this.notify("active");
+ this.dbusObj?.emit_property_changed(
+ "active",
+ GLib.Variant.new_boolean(value),
+ );
+ if (!value) {
+ // Whenever a workspace is deactivated, all other flags should be unset.
+ this.focused = false;
+ this.visible = false;
+ this.urgent = false;
+ }
+ }
+
+ @property(Boolean)
+ get focused() {
+ return this.#focused;
+ }
+
+ set focused(value: boolean) {
+ if (this.#focused === value) {
+ return;
+ }
+ this.#focused = value;
+ this.notify("focused");
+ this.dbusObj?.emit_property_changed(
+ "focused",
+ GLib.Variant.new_boolean(value),
+ );
+ }
+
+ @property(Boolean)
+ get visible() {
+ return this.#visible;
+ }
+
+ set visible(value: boolean) {
+ if (this.#visible === value) {
+ return;
+ }
+ this.#visible = value;
+ this.notify("visible");
+ this.dbusObj?.emit_property_changed(
+ "visible",
+ GLib.Variant.new_boolean(value),
+ );
+ }
+
+ @property(Boolean)
+ get urgent() {
+ return this.#urgent;
+ }
+
+ set urgent(value: boolean) {
+ if (this.#urgent === value) {
+ return;
+ }
+ this.#urgent = value;
+ this.notify("urgent");
+ this.dbusObj?.emit_property_changed(
+ "urgent",
+ GLib.Variant.new_boolean(value),
+ );
+ }
+
+ @property(String)
+ get name() {
+ return this.definition.name;
+ }
+
+ @property(Group)
+ /**
+ * The group that this workspace is currently assigned to.
+ *
+ * While a workspace can be in multiple groups, including multiple groups
+ * in the same context, it can only be assigned to one group at a time. If
+ * activated in a different group in the same context, it will be reassigned
+ * to that group.
+ *
+ * An inactive workspace will not have a current group, even if it only belongs
+ * to a single group in the active context.
+ */
+ get currentGroup() {
+ return this.#currentGroup;
+ }
+
+ set currentGroup(group: Group | null) {
+ if (group !== this.#currentGroup) {
+ this.#currentGroup = group;
+ this.notify("current-group");
+ }
+ }
+
+ @property(String)
+ get currentOutput() {
+ return this.#currentGroup?.outputName ?? null;
+ }
+
+ set currentOutput(output) {
+ if (output !== this.#currentOutput) {
+ this.#currentOutput = output;
+ this.notify("current-output");
+ }
+ }
+
+ deactivate() {
+ console.log(`Deactivating workspace ${this.definition.index}`);
+ this.active = false;
+ }
+
+ sync(update: types.WorkspaceResponse) {
+ this.active = true;
+ this.visible = update.visible;
+ this.focused = update.focused;
+ this.urgent = update.urgent;
+ }
+
+ async relocate(to: Group, retainFocus: boolean = false) {
+ if (this.#currentGroup === to) {
+ return;
+ }
+
+ await Sway.instance.command(
+ `workspace ${this.definition.index}; move workspace to output ${to.outputName}`,
+ );
+ }
+
+ async focus(to?: Group) {
+ if (this.#currentGroup === to || to === undefined) {
+ await Sway.instance.command(`workspace ${this.definition.index}`);
+ } else {
+ await this.relocate(to);
+ }
+ }
+
+ async moveContainerTo() {
+ await Sway.instance.command(
+ `move container to workspace ${this.definition.index}`,
+ );
+ }
+}
+
+@register({
+ GTypeName: "SwayWorkspaceGroup",
+})
+export class Group extends DBusObject {
+ #workspaces: Workspace[] = [];
+ #context: Context;
+ #outputName: string | null = null;
+
+ #monitor: Gdk.Monitor | null = null;
+
+ @property(Context)
+ get context() {
+ return this.#context;
+ }
+
+ @property(Workspace)
+ get focusedWorkspace() {
+ return this.#workspaces.find((workspace) => workspace.visible);
+ }
+
+ @property(Workspace)
+ get focused() {
+ return this.focusedWorkspace?.focused ?? false;
+ }
+
+ @property(Gdk.Monitor)
+ get monitor() {
+ return this.#monitor;
+ }
+
+ @property(String)
+ get outputName() {
+ return this.#outputName ?? "";
+ }
+
+ @property(Object)
+ get workspacePaths() {
+ return this.#workspaces.map((workspace) => workspace.objectPath);
+ }
+
+ @property(String)
+ get focusedWorkspacePath() {
+ return this.focusedWorkspace?.objectPath ?? "";
+ }
+
+ @signal()
+ declare gainedFocus: () => void;
+
+ @signal()
+ declare lostFocus: () => void;
+
+ @signal(Gdk.Monitor)
+ declare activated: (monitor: Gdk.Monitor) => void;
+
+ @signal()
+ declare deactivated: () => void;
+
+ constructor(
+ public readonly definition: defs.Group,
+ public readonly name: string,
+ public readonly outputDefinition: defs.Output,
+ context: Context,
+ ) {
+ super(
+ "dev.ezri.voidshell.workspaces.Group",
+ `/dev/ezri/VoidShell/workspaces/context/${context.name}/group/${name}`,
+ );
+ this.#context = context;
+ this.#workspaces = definition.workspaces
+ .map((index) => Tree.instance.workspaces.get(index.toString()))
+ .filter((workspace) => workspace !== undefined);
+
+ this.#workspaces.forEach((workspace) => {
+ workspace.connect(
+ "notify::visible",
+ // Bind the function, just to be safe.
+ (() => {
+ if (workspace.visible) {
+ this.notify("focused-workspace");
+ }
+ }).bind(this),
+ );
+ workspace.connect(
+ "notify::focused",
+ (() => {
+ if (workspace === this.focusedWorkspace) {
+ this.notify("focused");
+ }
+ }).bind(this),
+ );
+ });
+ }
+
+ /**
+ * Compute the score of this group based on the given Sway output.
+ */
+ score(output: types.OutputResponse): [number, number] {
+ if (
+ this.outputDefinition.make === output.make &&
+ this.outputDefinition.model === output.model &&
+ this.outputDefinition.serial === output.serial
+ ) {
+ // Perfect match scores 3.
+ return [3, 0];
+ }
+ const nameMatch =
+ this.outputDefinition.names?.findIndex((name) => name === output.name) ??
+ -1;
+ if (nameMatch !== -1) {
+ // Name match scores 2.
+ return [2, nameMatch];
+ }
+ if (this.outputDefinition.names?.includes("*")) {
+ // Wildcard match scores 1.
+ return [1, 0];
+ }
+ return [0, 0];
+ }
+
+ getOutputPreferences(outputs: types.OutputResponse[]) {
+ // Highest preference goes to a perfect match
+ const scores = outputs
+ .map((output) => [output, ...this.score(output)] as const)
+ .sort((a, b) => {
+ if (a[1] === b[1]) {
+ return a[2] - b[2];
+ }
+ return b[1] - a[1];
+ })
+ .filter(([_, score]) => score > 0);
+ return scores;
+ }
+
+ async activate(output: types.OutputResponse) {
+ // Ensure that this output is compatible with the group.
+ const [score, _] = this.score(output);
+ if (score === 0) {
+ throw new Error(
+ `Output ${output.name} is not compatible with this group.`,
+ );
+ }
+
+ // Configure monitor
+ await Sway.instance.command(
+ `output ${output.name} pos ${this.outputDefinition.position[0]} ${this.outputDefinition.position[1]} ${this.outputDefinition.mode}`,
+ );
+
+ // Enable monitor.
+ await Sway.instance.command(`output ${output.name} enable`);
+
+ // Store monitor name.
+ this.#outputName = output.name;
+
+ // Retrieve GDK monitor.
+ const monitor =
+ Gdk.Display.get_default()?.get_monitor_at_point(
+ this.outputDefinition.position[0],
+ this.outputDefinition.position[1],
+ ) ?? null;
+
+ // Notify listeners.
+ this.#monitor = monitor;
+ if (monitor !== null) {
+ this.activated(monitor);
+ }
+ this.notify("monitor");
+
+ // If the output's focused workspace is not in this group, focus the first workspace.
+ if (!this.#workspaces.some((workspace) => workspace.visible)) {
+ await this.#workspaces[0].focus(this);
+ }
+
+ // Add ourselves to each workspace
+ this.#workspaces.forEach((workspace) => workspace.groups.add(this));
+ }
+
+ async focus() {
+ if (this.#outputName === null) {
+ throw new Error("Group is not activated.");
+ }
+ await Sway.instance.command(`focus output ${this.#outputName}`);
+ }
+
+ getWorkspace(index: number): Workspace | undefined {
+ if (index >= this.#workspaces.length) {
+ return undefined;
+ }
+ return this.#workspaces[index];
+ }
+
+ async focusWorkspace(index: number): Promise {
+ const workspace = this.getWorkspace(index);
+ if (workspace === undefined) return false;
+ try {
+ await workspace.focus(this);
+ } catch {
+ return false;
+ }
+ return true;
+ }
+
+ deactivate() {
+ // Remove ourselves from every workspace
+ this.#workspaces.forEach((workspace) => workspace.groups.delete(this));
+ }
+}
+
+@register({
+ GTypeName: "SwayWorkspaceContext",
+})
+export class Context extends DBusObject {
+ #groups: Map = new Map();
+ #focusedGroup: Group | null = null;
+ #groupOrder: string[] = [];
+ #active: boolean = false;
+
+ constructor(
+ public readonly definition: defs.Context,
+ public readonly name: string,
+ ) {
+ super(
+ "dev.ezri.voidshell.workspaces.Context",
+ `/dev/ezri/VoidShell/workspaces/context/${name}`,
+ );
+ definition.outputs.forEach((output) => {
+ if (output.group === undefined || !definition.groups[output.group]) {
+ throw new Error(`Group ${output.group} not found in context`);
+ }
+ const group = new Group(
+ definition.groups[output.group],
+ output.group,
+ output,
+ this,
+ );
+ this.#groups.set(output.group, group);
+ });
+ // Group order is defined by the order in which the groups are defined in the context, not the order in which they are defined in the outputs.
+ this.#groupOrder = Object.keys(definition.groups);
+ }
+
+ @property(Group)
+ get focusedGroup() {
+ return this.#focusedGroup;
+ }
+
+ @property(Boolean)
+ get active() {
+ return this.#active;
+ }
+
+ getGroupOnOutput(output: string): Group | undefined {
+ return Array.from(this.#groups.values()).find(
+ (group) => group.outputName === output,
+ );
+ }
+
+ /**
+ * This function implements the output matching algorithm.
+ * @param outputSet The set of outputs to match against. Usually the currently connected outputs, as retrieved from Sway.
+ * @returns A map of group names to outputs and scores.
+ */
+ #matchGroupsWithOutputs(outputSet: types.OutputResponse[]) {
+ // First get preferences
+ const scores = Array.from(this.#groups.entries()).map(([name, group]) => {
+ return [[name, group], group.getOutputPreferences(outputSet)] as const;
+ });
+ // Create set of available outputs
+ const availableOutputs = new Set(outputSet);
+ // Create set of used groups
+ const usedGroups = new Set();
+ let i = 0;
+ let score = 3;
+ const result = new Map<
+ string,
+ [output: types.OutputResponse, score: number]
+ >();
+ while (availableOutputs.size > 0 && score > 0) {
+ const [[name, group], preferences] = scores[i];
+ if (usedGroups.has(group)) {
+ i++;
+ if (i >= scores.length) {
+ i = 0;
+ score--;
+ }
+ continue;
+ }
+ const [output, groupScore] = preferences.find(
+ ([output, s]) => s === score && availableOutputs.has(output),
+ ) ?? [undefined];
+ if (output !== undefined) {
+ availableOutputs.delete(output);
+ usedGroups.add(group);
+ result.set(name, [output, groupScore]);
+ if (score === 1) {
+ // Only one output can be matched with a wildcard
+ break;
+ }
+ }
+ }
+ // Verify that all groups have been matched
+ if (usedGroups.size !== this.#groups.size) {
+ return null;
+ }
+ return result;
+ }
+
+ /**
+ * Compute the score of this context based on the given Sway output set.
+ */
+ score(outputSet: types.OutputResponse[], includePriority = true) {
+ const match = this.#matchGroupsWithOutputs(outputSet);
+ if (match === null) {
+ return 0;
+ }
+ return Array.from(match.values()).reduce(
+ (acc, [_, score]) => acc + score,
+ includePriority ? (this.definition.priority ?? 0) : 0,
+ );
+ }
+
+ /**
+ * Activate this context.
+ */
+ async activate() {
+ const outputs = await Sway.instance.getOutputs();
+ const match = this.#matchGroupsWithOutputs(outputs);
+ if (match === null) {
+ throw new InvalidContextError(
+ this,
+ "Could not activate context: context is incompatible with current outputs.",
+ );
+ }
+ // Construct a set of output names that this context will use.
+ const usedOutputs = new Set(
+ Array.from(match.values()).map(([output]) => output.name),
+ );
+ if (Tree.instance.currentContext !== null) {
+ await Tree.instance.currentContext.deactivate(usedOutputs);
+ }
+ // Activate groups.
+ await Promise.all(
+ Array.from(match.entries()).map(async ([name, [output, _]]) => {
+ const group = this.#groups.get(name);
+ if (group === undefined) {
+ throw new InvalidContextError(
+ this,
+ `Could not activate context: group ${name} is not defined.`,
+ );
+ }
+ await group.activate(output);
+ }),
+ );
+ // Notify listeners.
+ this.#active = true;
+ this.notify("active");
+ Tree.instance.notify("current-context");
+ }
+
+ /**
+ * Deactivate this context.
+ * @param outputsToLeave A set of outputs that should not be disabled, selected by output name
+ */
+ async deactivate(outputsToLeave: Set) {
+ // First, notify for deactivation and move ourselves to the lowest priority in the event list
+ this.#active = false;
+ Tree.instance.emit("deactivating");
+ await Promise.resolve(); // This should allow the event loop to run and any event listeners to run before we continue
+
+ // Then, deactivate the outputs and groups.
+ const command = Array.from(this.#groups.values())
+ .map((group) => {
+ group.deactivate();
+ return group;
+ })
+ .filter((group) => !outputsToLeave.has(group.outputName ?? ""))
+ .map((group) => `output ${group.outputName} disable`)
+ .join("; ");
+ await Sway.instance.command(command);
+ }
+
+ getWorkspace(
+ index: number,
+ ): [workspace: Workspace, group: Group] | [null, null] {
+ const currentGroupIndex = Math.max(
+ this.#groupOrder.indexOf(this.focusedGroup?.name ?? ""),
+ 0,
+ );
+ const groupName = this.#groupOrder
+ .rotate(currentGroupIndex)
+ .find((name) => {
+ const group = this.#groups.get(name);
+ if (group === undefined) {
+ return false;
+ }
+ const workspace = group.getWorkspace(index);
+ if (workspace === this.focusedGroup?.focusedWorkspace) {
+ // If the workspace is the focused workspace of the focused group, don't return it; we want to search other outputs.
+ return false;
+ }
+ // If the workspace at this index exists on this output, return it. Otherwise, return the accumulator.
+ return !!workspace;
+ });
+ if (groupName === undefined) {
+ return [null, null];
+ }
+ const group = this.#groups.get(groupName) ?? null;
+ const workspace = group?.getWorkspace(index) ?? null;
+ if (workspace === null || group === null) {
+ return [null, null];
+ }
+ return [workspace, group];
+ }
+}
+
+@register({
+ GTypeName: "SwayWorkspaceTree",
+})
+export class Tree extends GObject.Object {
+ #workspaces: Map = new Map();
+ #contexts: Map = new Map();
+
+ static _instance: Tree | null = null;
+
+ public get workspaces() {
+ return this.#workspaces;
+ }
+
+ async #parseFile(path: string) {
+ console.log(`reading file ${path}`);
+ const contents = await readFileAsync(path);
+ const data = JSON.parse(contents) as defs.Config;
+ this.#workspaces = new Map();
+ this.#contexts = new Map();
+
+ // Create workspaces
+ data.workspaces.forEach((workspace) => {
+ this.#workspaces.set(
+ workspace.index.toString(),
+ new Workspace(workspace),
+ );
+ });
+
+ // Create contexts
+ Object.entries(data.contexts).forEach(([name, context]) => {
+ this.#contexts.set(name, new Context(context, name));
+ });
+
+ // Notify for tree refresh
+ this.emit("tree-refresh");
+
+ // If we're connected to Sway, immediately do a full tree sync. Otherwise,
+ // wait to be connected and then do a full tree sync.
+ if (Sway.instance.isConnected) {
+ this.sync();
+ } else {
+ const connId = Sway.instance.connect("connected", () => {
+ Sway.instance.disconnect(connId);
+ this.sync();
+ });
+ }
+ }
+
+ /**
+ * Synchronize the state of VoidShell's workspace tree with Sway
+ */
+ async sync() {
+ const touched = new Set();
+ try {
+ const workspaces = await Sway.instance.getWorkspaces();
+ workspaces.forEach((workspace) => {
+ const ws = this.#workspaces.get(workspace.name);
+ if (ws === undefined) {
+ this.#workspaces.set(workspace.name, Workspace.fromSync(workspace));
+ } else {
+ ws.sync(workspace);
+ touched.add(ws);
+ }
+ });
+ this.#workspaces.forEach((workspace) => {
+ if (!touched.has(workspace)) {
+ workspace.active = false;
+ }
+ });
+ } catch (e) {
+ console.error("Failed to sync", e);
+ }
+ }
+
+ static get instance() {
+ if (!this._instance) {
+ throw new Error("Tree instance not initialized");
+ }
+ return Tree._instance as Tree;
+ }
+
+ constructor(filepath: string) {
+ super();
+ console.log(`Parsing file ${filepath}`);
+ if (Tree._instance !== null) {
+ console.error("Tree instance already initialized...");
+ throw new Error("Tree instance already initialized");
+ }
+ Tree._instance = this;
+ console.log("Dereferencing symlink");
+ dereferenceSymbolicLink(filepath)
+ .then(async (resolvedPath) => {
+ console.log(`Resolved ${filepath} to ${resolvedPath}`);
+ try {
+ await this.#parseFile(resolvedPath);
+ } catch (e) {
+ console.error("Error parsing config file:", e);
+ }
+ monitorFile(filepath, async (_, event) => {
+ if (event === Gio.FileMonitorEvent.CHANGES_DONE_HINT) {
+ const oldWorkspaces = this.#workspaces;
+ const oldContexts = this.#contexts;
+ try {
+ await this.#parseFile(filepath);
+ } catch (e) {
+ console.error("Error parsing config file:", e);
+ this.#workspaces = oldWorkspaces;
+ this.#contexts = oldContexts;
+ }
+ }
+ });
+ })
+ .catch((reason) => {
+ console.error("Error dereferencing symlink:", reason);
+ });
+ Sway.instance.connect("workspace", (_, event: types.WorkspaceEvent) => {
+ this.sync();
+ });
+ }
+
+ @property(Context)
+ get currentContext(): Context | null {
+ return (
+ Array.from(this.#contexts.values()).find((context) => context.active) ??
+ null
+ );
+ }
+
+ @property(String)
+ get currentContextName(): string {
+ return (
+ Array.from(this.#contexts.entries()).find(
+ ([, context]) => context.active,
+ )?.[0] ?? ""
+ );
+ }
+
+ @signal()
+ /**
+ * Emitted when a context begins deactivating.
+ */
+ declare deactivating: () => void;
+
+ @signal()
+ /**
+ * Emitted after a full tree refresh which invalidates all context, group, and workspace objects.
+ */
+ declare treeRefresh: () => void;
+
+ async findNewContext() {
+ const outputs = await Sway.instance.getOutputs();
+ const topScore = Array.from(this.#contexts.values())
+ .map((context) => [context, context.score(outputs)] as const)
+ .sort(([, a], [, b]) => b - a)
+ .find(([_, score]) => score > 0)?.[0];
+ if (topScore === undefined) {
+ // No available context
+ return;
+ }
+ await topScore.activate();
+ }
+
+ async requestContext(name: string) {
+ const context = this.#contexts.get(name);
+ if (context === undefined) {
+ throw new Error(`Context ${name} not found`);
+ }
+ // Get current outputs
+ const outputs = await Sway.instance.getOutputs();
+ // Score the context
+ const score = context.score(outputs, false);
+ if (score > 0) {
+ await context.activate();
+ }
+ throw new Error(`Context ${name} is incompatible with current outputs`);
+ }
+
+ getWorkspace(
+ groupIndex: number,
+ ): [workspace: Workspace, group: Group] | [null, null] {
+ return this.currentContext?.getWorkspace(groupIndex) ?? [null, null];
+ }
+}
diff --git a/synchronization.ts b/synchronization.ts
new file mode 100644
index 0000000..3ffe88f
--- /dev/null
+++ b/synchronization.ts
@@ -0,0 +1,59 @@
+// This module contains async synchronization primitives
+//
+// Synchronization primitives are still needed in async code because any "await" or ".then()" call will
+// return to the event loop, potentially allowing another coroutine to
+
+/**
+ * Basic synchronization primitive that only allows a single coroutine to access a resource
+ * at a time.
+ */
+export class Mutex {
+ private locking = Promise.resolve();
+ private isLocked = false;
+
+ /**
+ * Get the current state of the mutex.
+ * Unlike multithreaded synchronization primitives, this can be queried safely without having to acquire
+ * the mutex, as asyncio multitasking is cooperative, meaning you don't need to worry about another task
+ * interrupting your function unless your function returns to the event loop via an 'await' statement.
+ */
+ get locked() {
+ return this.isLocked;
+ }
+
+ /**
+ * Acquire the mutex. This returns a function to release the mutex, which must be called
+ * or the application will deadlock.
+ *
+ * It is preferred to use the with() method instead of calling acquire() directly, which provides
+ * safeguards to ensure that the lock will always be released, however some logic and code flows
+ * will require the flexibility that calling acquire() directly gives.
+ */
+ async acquire() {
+ this.isLocked = true;
+ let unlockNext: () => void;
+ const willLock = new Promise((resolve) => (unlockNext = resolve));
+ willLock.then(() => (this.isLocked = false));
+ const willUnlock = this.locking.then(() => unlockNext);
+ this.locking = this.locking.then(() => willLock);
+ return willUnlock;
+ }
+
+ /**
+ * Executes the given function with the mutex acquired, and releases the mutex when the function exits.
+ *
+ * This is the preferred way to utilize a Mutex, as it ensures that it will not be left in a locked state.
+ */
+ async with(func: () => T): Promise {
+ const release = await this.acquire();
+ try {
+ let result = func();
+ while (result instanceof Promise) {
+ result = await result;
+ }
+ return result;
+ } finally {
+ release();
+ }
+ }
+}
diff --git a/tsconfig.json b/tsconfig.json
index 2d6a915..e6e0ec9 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,21 +1,21 @@
{
- "$schema": "https://json.schemastore.org/tsconfig",
- "compilerOptions": {
- "experimentalDecorators": true,
- "strict": true,
- "target": "ES2023",
- "moduleResolution": "Bundler",
- // "checkJs": true,
- // "allowJs": true,
- "jsx": "react-jsx",
- "jsxImportSource": "/usr/share/astal/gjs/gtk3",
- "paths": {
- "astal": [
- "/usr/share/astal/gjs"
- ],
- "astal/*": [
- "/usr/share/astal/gjs/*"
- ]
- },
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "compilerOptions": {
+ "experimentalDecorators": true,
+ "strict": true,
+ "target": "ES2023",
+ "moduleResolution": "Bundler",
+ // "checkJs": true,
+ // "allowJs": true,
+ "jsx": "react-jsx",
+ "jsxImportSource": "/usr/share/astal/gjs/gtk3",
+ "include": ["ambient.d.ts"],
+ "paths": {
+ "astal": ["/usr/share/astal/gjs"],
+ "astal/*": ["/usr/share/astal/gjs/*"],
+ "@/*": ["./*"],
+ "@widgets/*": ["./widget/*"],
+ "@services/*": ["./services/*"]
}
+ }
}
diff --git a/utils.ts b/utils.ts
new file mode 100644
index 0000000..6b4aa68
--- /dev/null
+++ b/utils.ts
@@ -0,0 +1,144 @@
+import Gio from "gi://Gio";
+import GLib from "gi://GLib";
+import { readFileAsync } from "astal/file";
+
+export async function readFromStreamRaw(
+ stream: Gio.InputStream,
+ bytes: number,
+): Promise {
+ return new Promise((resolve, reject) => {
+ stream.read_bytes_async(
+ bytes,
+ GLib.PRIORITY_DEFAULT,
+ null,
+ (stream, result) => {
+ try {
+ const data = stream!.read_bytes_finish(result);
+ const buffer = data.get_data();
+ if (buffer === null) {
+ reject(new Error("Failed to read from stream"));
+ return;
+ }
+ resolve(buffer);
+ } catch (e) {
+ reject(e);
+ }
+ },
+ );
+ });
+}
+
+export async function readFromStream(
+ stream: Gio.InputStream,
+ bytes: number,
+): Promise {
+ const chunkCount = Math.ceil(bytes / 4096);
+ const buffer = await Array.from({ length: chunkCount }, (_, i) => i).reduce(
+ async (acc, i) => {
+ const buffer = await acc;
+ const chunkSize = Math.min(4096, bytes - i * 4096);
+ const chunk = await readFromStreamRaw(stream, chunkSize);
+ buffer.set(chunk, i * 4096);
+ return buffer;
+ },
+ Promise.resolve(new Uint8Array(bytes)),
+ );
+ return buffer;
+}
+
+export async function writeToStream(
+ stream: Gio.OutputStream,
+ data: ArrayBuffer | Uint8Array,
+): Promise {
+ if (data instanceof ArrayBuffer) {
+ data = new Uint8Array(data);
+ }
+ return new Promise((resolve, reject) => {
+ stream.write_all_async(
+ data as Uint8Array,
+ GLib.PRIORITY_DEFAULT,
+ null,
+ (stream, result) => {
+ try {
+ stream?.write_all_finish(result) ??
+ reject(new Error("Failed to write to stream"));
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ },
+ );
+ });
+}
+
+export async function dereferenceSymbolicLink(
+ filename: string,
+): Promise {
+ return new Promise((resolve, reject) => {
+ console.log(`Dereferencing symbolic link for ${filename}`);
+ const file = Gio.File.new_for_path(filename);
+ file.query_info_async(
+ "standard::is-symlink,standard::symlink-target",
+ Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
+ GLib.PRIORITY_DEFAULT,
+ null,
+ (file, result) => {
+ try {
+ const info = file!.query_info_finish(result);
+ if (info.get_is_symlink()) {
+ console.log("File is a symlink");
+ const target = info.get_symlink_target() as string;
+ console.log(target);
+ if (target.startsWith("./") || target.startsWith("../")) {
+ resolve(file!.resolve_relative_path(target).get_path()!);
+ } else if (target.startsWith("/")) {
+ resolve(target);
+ } else {
+ resolve(file!.get_parent()!.get_child(target).get_path()!);
+ }
+ resolve(file!.resolve_relative_path(target).get_path()!);
+ } else {
+ resolve(filename);
+ }
+ } catch (e) {
+ console.error(e);
+ reject(e);
+ }
+ },
+ );
+ });
+}
+
+const dbusXml = new Map();
+
+export async function getDbusXml(ifaceName: string) {
+ if (dbusXml.has(ifaceName)) {
+ return dbusXml.get(ifaceName) as string;
+ }
+ const contents = await readFileAsync(`dbus/${ifaceName}.xml`);
+ dbusXml.set(ifaceName, contents);
+ return contents;
+}
+
+declare global {
+ // Add new "rotate" method to Array
+ interface Array {
+ /**
+ * Rotates the array by `n` positions.
+ *
+ * @param n The number of positions to rotate the array by.
+ * @returns The rotated array.
+ */
+ rotate(n: number): T[];
+ }
+}
+
+Array.prototype.rotate = function (this: T[], n: number): T[] {
+ const array = this;
+ const length = array.length;
+ if (length === 0) {
+ return [];
+ }
+ n = ((n % length) + length) % length;
+ return array.slice(n).concat(array.slice(0, n));
+};
diff --git a/widget/Bar.tsx b/widget/Bar.tsx
index 1d5b50f..70c304d 100644
--- a/widget/Bar.tsx
+++ b/widget/Bar.tsx
@@ -1,10 +1,13 @@
import { App, Astal, Gtk } from "astal/gtk3";
-import { Variable } from "astal";
+import { bind, Variable } from "astal";
import { SystemName } from "./system/Name";
+import { Tree } from "@/services/sway/workspaces";
const time = Variable("").poll(1000, "date");
export default function Bar(monitor: number) {
+ const tree = Tree.instance;
+ const currentContext = bind(tree, "currentContextName");
return (
-
+
diff --git a/widget/services/sway/ipc.ts b/widget/services/sway/ipc.ts
deleted file mode 100644
index 237bd93..0000000
--- a/widget/services/sway/ipc.ts
+++ /dev/null
@@ -1,312 +0,0 @@
-import GObject, { register, property, signal } from "astal/gobject";
-import { App } from "astal";
-import {
- WorkspaceResponse,
- OutputResponse,
- SwayMessage,
- SwayEvent,
- ModeEvent,
- WorkspaceEvent,
- OutputEvent,
-} from "./types";
-import Gdk from "gi://Gdk";
-import Gio from "gi://Gio";
-import GLib from "gi://GLib";
-import * as utils from "../../utils";
-
-@register({
- GTypeName: "Sway",
-})
-/**
- * Sway connection singleton that implements the sway IPC protocol at a low level.
- */
-export class Sway extends GObject.Object {
- private #commandSocket: Gio.SocketConnection | null = null;
- private #subscriptionSocket: Gio.SocketConnection | null = null;
-
- private #mode: string = "";
-
- @signal()
- /**
- * Emitted when the connection to sway is established
- */
- declare connected: () => void;
-
- @signal()
- /**
- * Emitted when the connection to sway is lost
- */
- declare disconnected: () => void;
-
- @signal(Array)
- /**
- * Emitted when we receive a subscription event
- */
- declare subscribed: (events: SwayEvent[]) => void;
-
- @signal(Object)
- /**
- * Emitted when we receive a workspace event
- */
- declare workspace: (event: WorkspaceEvent) => void;
-
- @signal(Object)
- /**
- * Emitted when we receive an output event
- */
- declare output: (event: OutputEvent) => void;
-
- @property(String)
- /**
- * The current binding mode of sway
- */
- get mode(): string {
- return this.#mode;
- }
-
- /**
- * Run a command on sway
- */
- public async command(command: string): Promise {
- const result = JSON.parse(
- await this.#sendRaw(this.#commandSocket, SwayMessage.RUN_COMMAND, command)
- );
- }
-
- /**
- * Get the current sway workspaces
- */
- public async getWorkspaces(): Promise {
- const result = JSON.parse(
- await this.#sendRaw(this.#commandSocket, SwayMessage.GET_WORKSPACES)
- );
- }
-
- /**
- * Get the current sway outputs
- */
- public async getOutputs(): Promise {
- const result = JSON.parse(
- await this.#sendRaw(this.#commandSocket, SwayMessage.GET_OUTPUTS)
- );
- }
-
- /**
- * Get the current sway tree
- */
- public async getTree(): Promise {
- const result = JSON.parse(
- await this.#sendRaw(this.#commandSocket, SwayMessage.GET_TREE)
- );
- }
-
- private constructor() {
- super();
- this.#connect();
- }
-
- private static #instance: Sway;
-
- /**
- * Get the default instance of Sway
- */
- static get_default() {
- if (!Sway.#instance) {
- Sway.#instance = new Sway();
- }
- return Sway.#instance;
- }
-
- private async #createConnection(
- address: string
- ): Promise {
- const client = new Gio.SocketClient();
- return new Promise((resolve, reject) => {
- client.connect_to_uri_async(
- `unix:${address}`,
- 0,
- null,
- (client, result) => {
- try {
- const connection = client.connect_to_uri_finish(result);
- resolve(connection);
- } catch (e) {
- reject(e);
- }
- }
- );
- });
- }
-
- private async #getMode(): Promise {
- const result = JSON.parse(
- await this.#sendRaw(this.#commandSocket, SwayMessage.GET_BINDING_STATE)
- );
- return result.name;
- }
-
- private async #connect() {
- const address = GLib.getenv("SWAYSOCK");
- if (!address) {
- throw new Error("SWAYSOCK not set");
- }
- const socket = new Gio.SocketClient();
- // Create two connections, one for commands and one for subscriptions. This is so we
- // can easily distinguish between responses to commands and subscription events.
- const command = await this.#createConnection(address);
- const subscription = await this.#createConnection(address);
-
- this.#commandSocket = command;
- this.#subscriptionSocket = subscription;
- this.connected();
-
- // Get the current mode
- this.#mode = await this.#getMode();
- this.notify("mode");
-
- this.#startSubscriberListen();
- }
-
- static readonly MAGIC = "i3-ipc";
- static readonly HEADER_SIZE = Sway.MAGIC.length + 8;
- static readonly CHUNK_SIZE = 4096;
-
- private #startSubscriberListen() {
- if (!this.#subscriptionSocket) {
- throw new Error("Not connected");
- }
- this.#subscribe(
- SwayEvent.OUTPUT,
- SwayEvent.WORKSPACE,
- SwayEvent.MODE,
- SwayEvent.SHUTDOWN
- );
- const inputStream = this.#subscriptionSocket.input_stream;
- const reader = new Gio.DataInputStream({ base_stream: inputStream });
- const read = async () => {
- while (true) {
- try {
- const [event, payload] = await this.#readRaw(
- this.#subscriptionSocket
- );
- switch (event) {
- case SwayEvent.OUTPUT:
- const event = JSON.parse(payload) as OutputResponse;
- this.output(event);
- break;
- case SwayEvent.WORKSPACE:
- const event = JSON.parse(payload) as WorkspaceResponse;
- this.workspace(event);
- break;
- case SwayEvent.MODE:
- const event = JSON.parse(payload) as ModeResponse;
- this.mode(event);
- break;
- case SwayEvent.SHUTDOWN:
- this.disconnected();
- break;
- default:
- console.warn("Unhandled event", event);
- break;
- }
- } catch (e) {
- console.error("Failed to read event", e);
- break;
- }
- }
- };
- read();
- }
-
- private async #readHeader(
- stream: Gio.InputStream
- ): Promise<{ length: number; type: SwayMessage | SwayEvent }> {
- // We read one byte at a time until we have constructed a full magic string, then we read the rest of the header
- // This allows us to handle partial reads and corrupted data gracefully
- let idx = 0;
- const magic = new Uint8Array(Sway.MAGIC.length);
- while (idx < Sway.MAGIC.length) {
- const buffer = await utils.readFromStreamRaw(stream, 1);
- const byte = buffer[0];
- if (byte !== Sway.MAGIC.charCodeAt(idx)) {
- // Reset if we don't match the magic string
- idx = 0;
- } else {
- // Otherwise, keep going
- idx++;
- }
- }
- const header = new DataView(await readFromStream(stream, 8).buffer);
- const length = header.getUint32(0, true);
- const type = header.getUint32(4, true);
- return { length, type };
- }
-
- private async #readRaw(
- socket: Gio.SocketConnection
- ): Promise<[SwayMessage | SwayEvent, string]> {
- const inputStream = socket.input_stream;
-
- const { length, type } = await this.#readHeader(inputStream);
- const payloadBuf = utils.readFromStream(inputStream, length);
- const payload = new TextDecoder().decode(payloadBuf);
-
- return [type as SwayMessage | SwayEvent, payload];
- }
-
- private async #sendRaw(
- socket: Gio.SocketConnection | null,
- type: SwayMessage,
- payload: string = ""
- ): Promise {
- if (!socket || !socket.is_connected()) {
- throw new Error("Not connected");
- }
- if (socket === this.#commandSocket && type === SwayMessage.SUBSCRIBE) {
- throw new Error("Cannot subscribe on command socket");
- }
- // Construct the message
- const buffer = new ArrayBuffer(Sway.HEADER_SIZE + payload.length);
- const magicView = new Uint8Array(buffer, 0, Sway.MAGIC.length);
- const lengthView = new DataView(buffer, Sway.MAGIC.length, 4);
- const typeView = new DataView(buffer, Sway.MAGIC.length + 4, 4);
- const payloadView = new Uint8Array(buffer, Sway.HEADER_SIZE);
-
- magicView.set(Sway.MAGIC.split("").map((c) => c.charCodeAt(0)));
- lengthView.setUint32(0, payload.length, true);
- typeView.setUint32(0, type, true);
- payloadView.set(payload.split("").map((c) => c.charCodeAt(0)));
-
- const outputStream = this.#commandSocket.output_stream;
-
- // Send the message
- await utils.writeToStream(outputStream, buffer);
-
- // Read the response
- const [resultType, result] = await this.#readRaw(socket);
- if (resultType !== type) {
- throw new Error(`Unexpected response type: ${resultType}`);
- }
- return result;
- }
-
- private async #subscribe(...events: SwayEvent[]) {
- if (!this.#subscriptionSocket) {
- throw new Error("Not connected");
- }
- const eventNames = events.map((e) => SwayEvent[e].toLowerCase());
- const payload = JSON.stringify(eventNames);
- const result = JSON.parse(
- await this.#sendRaw(
- this.#subscriptionSocket,
- SwayMessage.SUBSCRIBE,
- payload
- )
- );
- if (result.success) {
- this.subscribed(events);
- } else {
- throw new Error(`Failed to subscribe: ${result}`);
- }
- }
-}
diff --git a/widget/services/sway/service.ts b/widget/services/sway/service.ts
deleted file mode 100644
index 4f8e7d1..0000000
--- a/widget/services/sway/service.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { App } from "astal";
-import GObject, { register, property, signal } from "astal/gobject";
-import { Sway } from "./ipc";
-import * as types from "./types";
-
-@register({
- GTypeName: "SwayService",
-})
-export class SwayService extends GObject.Object {
- private static #instance: SwayService;
-
- private constructor() {
- // Initialize the IPC and register event listeners
- Sway.get_default();
- }
-
- public static get_default(): SwayService {
- if (!SwayService.#instance) {
- SwayService.#instance = new SwayService();
- }
- return SwayService.#instance;
- }
-
- private #onWorkspaceEvent(event: types.WorkspaceEvent) {}
-}
diff --git a/widget/services/sway/workspaces.ts b/widget/services/sway/workspaces.ts
deleted file mode 100644
index 08397e0..0000000
--- a/widget/services/sway/workspaces.ts
+++ /dev/null
@@ -1,343 +0,0 @@
-import GObject, { register, property, signal } from "astal/gobject";
-import { App } from "astal";
-import { readFileAsync, monitorFile } from "astal/file";
-import * as types from "./types";
-import * as defs from "./definitions";
-import Gdk from "gi://Gdk";
-import Gio from "gi://Gio";
-import GLib from "gi://GLib";
-import { Sway } from "./ipc";
-
-@register({
- GTypeName: "SwayWorkspace",
-})
-export class Workspace extends GObject.Object {
- private #active: boolean = false;
- private #focused: boolean = false;
- private #visible: boolean = false;
- private #urgent: boolean = false;
-
- private #groups: Group[] = [];
- private #currentGroup: Group | null = null;
- private #currentOutput: string | null = null;
-
- constructor(public readonly definition: defs.Workspace) {
- super();
- }
-
- @property(Boolean)
- get active() {
- return this.#active;
- }
-
- @property(Boolean)
- get focused() {
- return this.#focused;
- }
-
- @property(Boolean)
- get visible() {
- return this.#visible;
- }
-
- @property(Boolean)
- get urgent() {
- return this.#urgent;
- }
-
- @property({ $gtype: "SwayWorkspaceGroup" })
- /**
- * The group that this workspace is currently assigned to.
- *
- * While a workspace can be in multiple groups, including multiple groups
- * in the same context, it can only be assigned to one group at a time. If
- * activated in a different group in the same context, it will be reassigned
- * to that group.
- *
- * An inactive workspace will not have a current group, even if it only belongs
- * to a single group in the active context.
- */
- get currentGroup() {
- return this.#currentGroup;
- }
-
- @property(String)
- get currentOutput() {
- return this.#currentGroup?.outputName ?? null;
- }
-
- handleEvent(event: types.WorkspaceEvent) {
- if (
- event.change === types.WorkspaceEventChange.EMPTY &&
- event.current?.name == this.definition.index.toString()
- ) {
- // If the workspace was emptied, deactivate it.
- this.#active = false;
- this.#focused = false;
- this.#visible = false;
- this.#urgent = false;
- this.#currentGroup = null;
- this.#currentOutput = null;
- this.notify("active");
- this.notify("focused");
- this.notify("visible");
- this.notify("urgent");
- this.notify("current-group");
- this.notify("current-output");
- return;
- }
- const update =
- event.current?.name === this.definition.index.toString()
- ? event.current
- : event.old;
- if (update !== undefined) {
- this.parseUpdate(update);
- }
- }
-
- parseUpdate(update: types.WorkspaceResponse) {
- if (!this.#active) {
- this.#active = true;
- this.notify("active");
- }
-
- if (this.#focused !== update.focused) {
- this.#focused = update.focused;
- this.notify("focused");
- }
-
- if (this.#visible !== update.visible) {
- this.#visible = update.visible;
- this.notify("visible");
- }
-
- if (this.#urgent !== update.urgent) {
- this.#urgent = update.urgent;
- this.notify("urgent");
- }
-
- if (this.#currentOutput !== update.output) {
- this.#currentGroup =
- this.#groups.find((group) => group.outputName === update.output) ??
- // Default to null, rather than undefined.
- null;
- this.notify("current-group");
- this.notify("current-output");
- }
- }
-
- async relocate(to: Group) {
- if (this.#currentGroup === to) {
- return;
- }
- if (this.#active) {
- // If the workspace is active, we need to move it to the new group.
- await Sway.command(
- `workspace ${this.#index}; move workspace to output ${to.outputName}`
- );
- } else {
- // If the workspace is inactive, we can simply assign it to the new group.
- await Sway.command(`workspace ${this.#index} output ${to.outputName}`);
- }
- }
-}
-
-@register({
- GTypeName: "SwayWorkspaceGroup",
-})
-export class Group extends GObject.Object {
- private #workspaces: Workspace[] = [];
- private #context: Context;
- private #outputName: string | null = null;
-
- private #monitor: Gdk.Monitor | null = null;
-
- @property({ $gtype: "SwayWorkspaceContext" })
- get context() {
- return this.#context;
- }
-
- @property(Workspace)
- get focusedWorkspace() {
- return this.#workspaces.find((workspace) => workspace.visible);
- }
-
- @property(Workspace)
- get focused() {
- return this.focusedWorkspace?.focused ?? false;
- }
-
- @property(Gdk.Monitor)
- get monitor() {
- return this.#monitor;
- }
-
- @signal()
- declare focused: () => void;
-
- @signal()
- declare unfocused: () => void;
-
- @signal(Gdk.Monitor)
- declare activated: (monitor: Gdk.Monitor) => void;
-
- @signal()
- declare deactivated: () => void;
-
- constructor(
- public readonly definition: defs.Group,
- public readonly outputDefinition: defs.Output,
- context: Context
- ) {
- super();
- this.#context = context;
- this.#workspaces = definition.workspaces.map((index) =>
- Tree.instance.workspaces.get(index.toString())
- );
-
- this.#workspaces.forEach((workspace) => {
- workspace.connect(
- "notify::visible",
- // Bind the function, just to be safe.
- (() => {
- if (workspace.visible) {
- this.notify("focused-workspace");
- }
- }).bind(this)
- );
- workspace.connect(
- "notify::focused",
- (() => {
- if (workspace === this.focusedWorkspace) {
- this.notify("focused");
- }
- }).bind(this)
- );
- });
- }
-
- /**
- * Compute the score of this group based on the given Sway output.
- */
- score(output: types.OutputResponse): [number, number] {
- if (
- this.outputDefinition.make === output.make &&
- this.outputDefinition.model === output.model &&
- this.outputDefinition.serial === output.serial
- ) {
- // Perfect match scores 3.
- return [3, 0];
- }
- const nameMatch = this.outputDefinition.names.findIndex(
- (name) => name === output.name
- );
- if (nameMatch !== -1) {
- // Name match scores 2.
- return [2, nameMatch];
- }
- if (this.outputDefinition.names.includes("*")) {
- // Wildcard match scores 1.
- return [1, 0];
- }
- return [0, 0];
- }
-
- getOutputPreferences(outputs: types.OutputResponse[]) {
- // Highest preference goes to a perfect match
- const scores = outputs
- .map((output) => [output, ...this.score(output)] as const)
- .sort((a, b) => {
- if (a[1] === b[1]) {
- return a[2] - b[2];
- }
- return b[1] - a[1];
- })
- .filter(([_, score]) => score > 0)
- .map(([output]) => output);
- return scores;
- }
-
- async activate(output: types.OutputResponse) {
- // Ensure that this output is compatible with the group.
- const [score, _] = this.score(output);
- if (score === 0) {
- throw new Error(
- `Output ${output.name} is not compatible with this group.`
- );
- }
-
- // Configure monitor
- await Sway.get_default().command(
- `output ${output.name} pos ${this.outputDefinition.x} ${this.outputDefinition.y} ${this.outputDefinition.mode}`
- );
-
- // Enable monitor.
- await Sway.get_default().command(`output ${output.name} enable`);
-
- // Store monitor name.
- this.#outputName = output.name;
-
- // Retrieve GDK monitor.
- const monitor = Gdk.Display.get_default().get_monitor_at_point(
- this.outputDefinition.x,
- this.outputDefinition.y
- );
-
- // Notify listeners.
- this.#monitor = monitor;
- this.activated(monitor);
- this.notify("monitor");
- }
-
- async focus() {
- if (this.#outputName === null) {
- throw new Error("Group is not activated.");
- }
- await Sway.get_default().command(`focus output ${this.#outputName}`);
- }
-}
-
-@register({
- GTypeName: "SwayWorkspaceContext",
-})
-class Context extends GObject.Object {
- private #groups: Map = new Map();
- private #focusedGroup: Group | null = null;
-
- constructor(public readonly definition: defs.Context) {
- super();
- }
-
- @property(Group)
- get focusedGroup() {
- return this.#focusedGroup;
- }
-}
-
-@register({
- GTypeName: "SwayWorkspaceTree",
-})
-class Tree extends GObject.Object {
- private #workspaces: Map = new Map();
- private #contexts: Map = new Map();
-
- private static #instance: Tree | null = null;
-
- public get workspaces() {
- return this.#workspaces;
- }
-
- private constructor() {}
-
- private async #parseFile(path: string) {
- const contents = await readFileAsync(path);
- const data = JSON.parse(contents) as defs.Config;
- }
-
- static get instance() {
- if (!Tree.#instance) {
- Tree.#instance = new Tree();
- }
- return Tree.#instance;
- }
-}
diff --git a/widget/system/Name.tsx b/widget/system/Name.tsx
index 44df243..5befd0a 100644
--- a/widget/system/Name.tsx
+++ b/widget/system/Name.tsx
@@ -1,4 +1,4 @@
-import { System } from "../services/System";
+import { System } from "@services/System";
export function SystemName() {
const system = System.get_default();
diff --git a/widget/utils.ts b/widget/utils.ts
deleted file mode 100644
index edabf51..0000000
--- a/widget/utils.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import Gio from "gi://Gio";
-import GLib from "gi://GLib";
-
-export async function readFromStreamRaw(
- stream: Gio.InputStream,
- bytes: number
-): Promise {
- return new Promise((resolve, reject) => {
- const buffer = new Uint8Array(bytes);
- stream.read_bytes_async(
- bytes,
- GLib.PRIORITY_DEFAULT,
- null,
- (stream, result) => {
- try {
- const data = stream.read_bytes_finish(result);
- data.get_data(buffer);
- resolve(buffer);
- } catch (e) {
- reject(e);
- }
- }
- );
- });
-}
-
-export async function readFromStream(
- stream: Gio.InputStream,
- bytes: number
-): Promise {
- return new Promise((resolve, reject) => {
- const chunkCount = Math.ceil(bytes / 4096);
- const buffer = await Array.from({ length: chunkCount }, (_, i) => i).reduce(
- (acc, i) => {
- const buffer = await acc;
- const chunkSize = Math.min(4096, bytes - i * 4096);
- const chunk = await readFromStreamRaw(stream, chunkSize);
- buffer.set(chunk, i * 4096);
- return buffer;
- },
- Promise.resolve(new Uint8Array(bytes))
- );
- });
-}
-
-export async function writeToStream(
- stream: Gio.OutputStream,
- data: ArrayBuffer | Uint8Array
-): Promise {
- if (data instanceof ArrayBuffer) {
- data = new Uint8Array(data);
- }
- return new Promise((resolve, reject) => {
- stream.write_all_async(
- data,
- GLib.PRIORITY_DEFAULT,
- null,
- (stream, result) => {
- try {
- stream.write_all_finish(result);
- resolve();
- } catch (e) {
- reject(e);
- }
- }
- );
- });
-}