From 23b63fa58df1ef0f62830c02be0d3f0f248b841c Mon Sep 17 00:00:00 2001 From: Ezri Brimhall Date: Tue, 19 Nov 2024 14:42:30 -0700 Subject: [PATCH] First update in a while lol --- .eslintrc | 4 + ambient.d.ts | 4 + app.ts | 23 +- dbus/dbus.ts | 0 .../dev.ezri.voidshell.workspaces.Context.xml | 3 + dbus/dev.ezri.voidshell.workspaces.Group.xml | 12 + ...ev.ezri.voidshell.workspaces.Workspace.xml | 16 + package-lock.json | 1406 +++++++++++++++++ package.json | 22 + {widget/services => services}/System.ts | 4 +- services/dbus.ts | 42 + .../services => services}/sway/definitions.ts | 3 +- services/sway/exceptions.ts | 10 + services/sway/ipc.ts | 349 ++++ services/sway/service.ts | 26 + {widget/services => services}/sway/types.ts | 6 +- services/sway/workspaces.ts | 807 ++++++++++ synchronization.ts | 59 + tsconfig.json | 36 +- utils.ts | 144 ++ widget/Bar.tsx | 9 +- widget/services/sway/ipc.ts | 312 ---- widget/services/sway/service.ts | 25 - widget/services/sway/workspaces.ts | 343 ---- widget/system/Name.tsx | 2 +- widget/utils.ts | 68 - 26 files changed, 2949 insertions(+), 786 deletions(-) create mode 100644 .eslintrc create mode 100644 ambient.d.ts create mode 100644 dbus/dbus.ts create mode 100644 dbus/dev.ezri.voidshell.workspaces.Context.xml create mode 100644 dbus/dev.ezri.voidshell.workspaces.Group.xml create mode 100644 dbus/dev.ezri.voidshell.workspaces.Workspace.xml create mode 100644 package-lock.json create mode 100644 package.json rename {widget/services => services}/System.ts (93%) create mode 100644 services/dbus.ts rename {widget/services => services}/sway/definitions.ts (98%) create mode 100644 services/sway/exceptions.ts create mode 100644 services/sway/ipc.ts create mode 100644 services/sway/service.ts rename {widget/services => services}/sway/types.ts (98%) create mode 100644 services/sway/workspaces.ts create mode 100644 synchronization.ts create mode 100644 utils.ts delete mode 100644 widget/services/sway/ipc.ts delete mode 100644 widget/services/sway/service.ts delete mode 100644 widget/services/sway/workspaces.ts delete mode 100644 widget/utils.ts 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 ( - +