buninu-browser 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSES/jsmdcui.txt +22 -0
- package/README.md +40 -4
- package/frontends/wayland/LICENSE-land +21 -0
- package/frontends/wayland/README.md +152 -0
- package/frontends/wayland/casty/LICENSE +21 -0
- package/frontends/wayland/casty/bookmarks.js +32 -0
- package/frontends/wayland/casty/click.js +96 -0
- package/frontends/wayland/casty/commands.js +61 -0
- package/frontends/wayland/casty/config.js +45 -0
- package/frontends/wayland/casty/download-directory.js +28 -0
- package/frontends/wayland/casty/download.js +49 -0
- package/frontends/wayland/casty/hints.js +334 -0
- package/frontends/wayland/casty/keys.js +56 -0
- package/frontends/wayland/casty/page-commands.js +80 -0
- package/frontends/wayland/casty/prompt-width.js +148 -0
- package/frontends/wayland/casty/urlbar.js +606 -0
- package/frontends/wayland/client.js +88 -0
- package/frontends/wayland/clipboard.js +80 -0
- package/frontends/wayland/index.js +130 -0
- package/frontends/wayland/input.js +75 -0
- package/frontends/wayland/protocol.js +55 -0
- package/frontends/wayland/protocols/wayland.xml +3248 -0
- package/frontends/wayland/protocols/xdg-shell.xml +1415 -0
- package/frontends/wayland/transport.js +120 -0
- package/frontends/wayland/urlbar.js +162 -0
- package/index.js +28 -1
- package/lib/audio/shared-connection.js +31 -0
- package/lib/cdp-server.js +15 -2
- package/lib/clipboard-command.js +10 -0
- package/lib/clipboard-powershell.js +22 -0
- package/lib/clipboard.js +37 -2
- package/lib/document-types.js +11 -0
- package/lib/headless-shell.js +54 -12
- package/lib/layout/text-layout.js +17 -9
- package/lib/paint/display-list.js +6 -1
- package/lib/qml/README.md +1427 -0
- package/lib/qml/builtins.js +771 -0
- package/lib/qml/canvas-dom.js +115 -0
- package/lib/qml/canvas.js +76 -0
- package/lib/qml/document.js +1199 -0
- package/lib/qml/fonts.js +69 -0
- package/lib/qml/format.js +9 -0
- package/lib/qml/host.js +352 -0
- package/lib/qml/images.js +199 -0
- package/lib/qml/launcher.js +132 -0
- package/lib/qml/loader.js +521 -0
- package/lib/qml/media-renderer.js +49 -0
- package/lib/qml/media.js +86 -0
- package/lib/qml/models.js +622 -0
- package/lib/qml/parser.js +381 -0
- package/lib/qml/qs +5 -0
- package/lib/qml/qscasty +3 -0
- package/lib/qml/qsheadless +3 -0
- package/lib/qml/qswayland +3 -0
- package/lib/qml/reactive.js +182 -0
- package/lib/qml/runtime.js +3603 -0
- package/lib/qml/shader/fragment.js +83 -0
- package/lib/qml/shader/qsb.js +27 -0
- package/lib/qml/shader/renderer.js +39 -0
- package/lib/qml/shader/runtime.js +61 -0
- package/lib/qml/shapes.js +251 -0
- package/lib/qml/shortcuts.js +65 -0
- package/lib/qml/text.js +109 -0
- package/lib/qml/textures.js +126 -0
- package/lib/qml/wayland-clipboard.js +32 -0
- package/lib/qml/window-constraints.js +16 -0
- package/lib/qml/worker-thread.js +70 -0
- package/lib/render-tree/index.js +16 -1
- package/lib/renderer/bindings.js +16 -1
- package/lib/renderer/canvas-2d.js +29 -0
- package/lib/renderer/document-selection.js +19 -0
- package/lib/renderer/host.js +45 -17
- package/lib/renderer/main.js +21 -6
- package/lib/renderer/page-realm.js +20 -10
- package/lib/renderer/page-renderer.js +497 -42
- package/lib/renderer/pointer-move.js +5 -3
- package/lib/renderer/render-interval.js +9 -0
- package/lib/renderer/text-input-paint.js +54 -0
- package/lib/renderer/text-input.js +57 -0
- package/lib/style/computed-style.js +10 -4
- package/package.json +1 -1
- package/test/audio-connection-lifetime.test.js +46 -0
- package/test/canvas-paint.test.js +8 -0
- package/test/clipboard.test.js +19 -0
- package/test/computed-style.test.js +12 -0
- package/test/document-selection.test.js +27 -0
- package/test/double-click.test.js +85 -0
- package/test/fixtures/canvas-damage.js +40 -0
- package/test/fixtures/fake-renderer.js +1 -0
- package/test/fixtures/flea-helpers/Dockerfile +36 -0
- package/test/fixtures/flea-helpers/bat.mjs +43 -0
- package/test/fixtures/flea-helpers/bin/bat +3 -0
- package/test/fixtures/flea-helpers/bin/bwrap +10 -0
- package/test/fixtures/flea-helpers/bin/bwrap-real-filter +9 -0
- package/test/fixtures/flea-helpers/bin/glow +3 -0
- package/test/fixtures/flea-helpers/bin/qs +3 -0
- package/test/fixtures/flea-helpers/bin/unsupported +3 -0
- package/test/fixtures/flea-helpers/check-highlighting.mjs +47 -0
- package/test/fixtures/flea-helpers/check-media.mjs +29 -0
- package/test/fixtures/flea-helpers/check-preview.mjs +27 -0
- package/test/fixtures/flea-helpers/check.mjs +36 -0
- package/test/fixtures/flea-helpers/highlight-cases.json +227 -0
- package/test/fixtures/flea-helpers/source-mime.xml +28 -0
- package/test/fixtures/flea-helpers/syntax-compat.mjs +23 -0
- package/test/fixtures/flea-thumbnail-stub/flea-placeholder.thumbnailer +4 -0
- package/test/fixtures/flea-thumbnail-stub/flea-thumbnail-stub +3 -0
- package/test/fixtures/omastorm-helpers/Dockerfile +35 -0
- package/test/fixtures/omastorm-helpers/check.mjs +56 -0
- package/test/fixtures/omastorm-helpers/launch.mjs +24 -0
- package/test/fixtures/omastorm-helpers/prepare.mjs +17 -0
- package/test/fixtures/omastorm-helpers/runtime.mjs +32 -0
- package/test/fixtures/pointer-paint-batching.js +23 -0
- package/test/fixtures/qml/flea/EscapeHint.qml +11 -0
- package/test/fixtures/qml/flea/LICENSE +21 -0
- package/test/fixtures/qml/flea/README.md +25 -0
- package/test/fixtures/qml/flea/Theme.qml +8 -0
- package/test/fixtures/qml/flea/demo.qml +24 -0
- package/test/fixtures/qml/flea/qmldir +3 -0
- package/test/fixtures/qml/flea-theme/LICENSE-Flea +21 -0
- package/test/fixtures/qml/flea-theme/LICENSE-Omarchy +20 -0
- package/test/fixtures/qml/flea-theme/README.md +70 -0
- package/test/fixtures/qml/flea-theme/button.qml +17 -0
- package/test/fixtures/qml/flea-theme/demo.qml +27 -0
- package/test/fixtures/qml/flea-theme/field.qml +21 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/BorderGeometry.js +373 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Color.qml +258 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/I18n.qml +118 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Numbers.js +19 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Plural.js +91 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Style.qml +561 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Util.qml +188 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/qmldir +13 -0
- package/test/fixtures/qml/flea-theme/segment.qml +17 -0
- package/test/fixtures/qml/flea-theme/settings.qml +58 -0
- package/test/fixtures/qml/flea-theme/text-metrics.qml +10 -0
- package/test/fixtures/qml/flea-theme/ui/DialogButton.qml +150 -0
- package/test/fixtures/qml/flea-theme/ui/DialogField.qml +147 -0
- package/test/fixtures/qml/flea-theme/ui/EscapeHint.qml +11 -0
- package/test/fixtures/qml/flea-theme/ui/Glass.qml +114 -0
- package/test/fixtures/qml/flea-theme/ui/Glyph.qml +54 -0
- package/test/fixtures/qml/flea-theme/ui/GlyphSolid.qml +22 -0
- package/test/fixtures/qml/flea-theme/ui/Reveal.qml +54 -0
- package/test/fixtures/qml/flea-theme/ui/SettingsRuler.qml +85 -0
- package/test/fixtures/qml/flea-theme/ui/SettingsSegment.qml +85 -0
- package/test/fixtures/qml/flea-theme/ui/StableModel.qml +89 -0
- package/test/fixtures/qml/flea-theme/ui/Theme.qml +460 -0
- package/test/fixtures/qml/flea-theme/ui/ThemeColors.qml +43 -0
- package/test/fixtures/qml/flea-theme/ui/ViewState.qml +565 -0
- package/test/fixtures/qml/flea-theme/ui/js/Anchor.js +379 -0
- package/test/fixtures/qml/flea-theme/ui/js/AnchorHold.js +162 -0
- package/test/fixtures/qml/flea-theme/ui/js/Archive.js +157 -0
- package/test/fixtures/qml/flea-theme/ui/js/BackReturn.js +110 -0
- package/test/fixtures/qml/flea-theme/ui/js/Buttons.js +54 -0
- package/test/fixtures/qml/flea-theme/ui/js/ColumnMenu.js +111 -0
- package/test/fixtures/qml/flea-theme/ui/js/Columns.js +298 -0
- package/test/fixtures/qml/flea-theme/ui/js/Contrast.js +157 -0
- package/test/fixtures/qml/flea-theme/ui/js/Density.js +44 -0
- package/test/fixtures/qml/flea-theme/ui/js/DirSizes.js +129 -0
- package/test/fixtures/qml/flea-theme/ui/js/ExtThumbs.js +115 -0
- package/test/fixtures/qml/flea-theme/ui/js/Filter.js +284 -0
- package/test/fixtures/qml/flea-theme/ui/js/FolderSorts.js +108 -0
- package/test/fixtures/qml/flea-theme/ui/js/Format.js +305 -0
- package/test/fixtures/qml/flea-theme/ui/js/Icons.js +206 -0
- package/test/fixtures/qml/flea-theme/ui/js/Input.js +39 -0
- package/test/fixtures/qml/flea-theme/ui/js/KeyName.js +64 -0
- package/test/fixtures/qml/flea-theme/ui/js/Keymap.js +370 -0
- package/test/fixtures/qml/flea-theme/ui/js/Kinds.js +145 -0
- package/test/fixtures/qml/flea-theme/ui/js/Lists.js +11 -0
- package/test/fixtures/qml/flea-theme/ui/js/MakeDefault.js +186 -0
- package/test/fixtures/qml/flea-theme/ui/js/Match.js +27 -0
- package/test/fixtures/qml/flea-theme/ui/js/Material.js +109 -0
- package/test/fixtures/qml/flea-theme/ui/js/Menu.js +513 -0
- package/test/fixtures/qml/flea-theme/ui/js/Motion.js +16 -0
- package/test/fixtures/qml/flea-theme/ui/js/Mounts.js +320 -0
- package/test/fixtures/qml/flea-theme/ui/js/MouseNav.js +13 -0
- package/test/fixtures/qml/flea-theme/ui/js/Nav.js +328 -0
- package/test/fixtures/qml/flea-theme/ui/js/Palette.js +33 -0
- package/test/fixtures/qml/flea-theme/ui/js/Places.js +236 -0
- package/test/fixtures/qml/flea-theme/ui/js/Protocols.js +296 -0
- package/test/fixtures/qml/flea-theme/ui/js/RecentMode.js +154 -0
- package/test/fixtures/qml/flea-theme/ui/js/Search.js +396 -0
- package/test/fixtures/qml/flea-theme/ui/js/Settings.js +476 -0
- package/test/fixtures/qml/flea-theme/ui/js/SettingsAbout.js +99 -0
- package/test/fixtures/qml/flea-theme/ui/js/SettingsShelf.js +52 -0
- package/test/fixtures/qml/flea-theme/ui/js/Sort.js +127 -0
- package/test/fixtures/qml/flea-theme/ui/js/StateWords.js +42 -0
- package/test/fixtures/qml/flea-theme/ui/js/Swap.js +166 -0
- package/test/fixtures/qml/flea-theme/ui/js/TextSize.js +107 -0
- package/test/fixtures/qml/flea-theme/ui/js/Thumbs.js +158 -0
- package/test/fixtures/qml/flea-theme/ui/js/UiState.js +438 -0
- package/test/fixtures/qml/flea-theme/ui/js/Update.js +132 -0
- package/test/fixtures/qml/flea-theme/ui/js/UserDirs.js +56 -0
- package/test/fixtures/qml/flea-theme/ui/qmldir +227 -0
- package/test/fixtures/qml/hello.qml +26 -0
- package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabel.qml +2 -0
- package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabelNew.qml +2 -0
- package/test/fixtures/qml/imports/Demo/Toolkit/qmldir +3 -0
- package/test/fixtures/qml/imports/components/Badge.qml +15 -0
- package/test/fixtures/qml/imports/logic/base.js +2 -0
- package/test/fixtures/qml/imports/logic/counter.js +4 -0
- package/test/fixtures/qml/imports/logic/math.js +3 -0
- package/test/fixtures/qml/imports/logic/shared.js +3 -0
- package/test/fixtures/qml/imports/main.qml +21 -0
- package/test/fixtures/qml/imports/theme/Theme.qml +8 -0
- package/test/fixtures/qml/imports/theme/qmldir +2 -0
- package/test/fixtures/qml/multiply.qml +61 -0
- package/test/fixtures/qml-canvas-dom.js +90 -0
- package/test/fixtures/qml-fonts/main.qml +10 -0
- package/test/fixtures/qml-locale/check.mjs +6 -0
- package/test/fixtures/qml-locale/main.qml +8 -0
- package/test/fixtures/qml-media-audio.js +39 -0
- package/test/fixtures/qml-shader/grab.qml +14 -0
- package/test/fixtures/qml-shader/gradient.frag +5 -0
- package/test/fixtures/qml-shader/main.qml +14 -0
- package/test/fixtures/qml-shader/texture-grab.qml +25 -0
- package/test/fixtures/qml-shader/texture-size.frag +5 -0
- package/test/fixtures/qml-shader-check.mjs +24 -0
- package/test/fixtures/title-no-paint.js +20 -0
- package/test/helpers/flea-sandbox.js +48 -0
- package/test/helpers/quickshell-fixture.js +125 -0
- package/test/html-clipboard-wayland.test.js +47 -0
- package/test/index.test.js +13 -0
- package/test/manual-casty-without-qml.js +87 -0
- package/test/manual-double-click.html +21 -0
- package/test/manual-flea-application.js +49 -0
- package/test/manual-flea-binary.js +62 -0
- package/test/manual-flea-qml.js +9 -0
- package/test/manual-flea-settings.js +40 -0
- package/test/manual-flea-theme.js +40 -0
- package/test/manual-mouse-buttons.html +18 -0
- package/test/manual-omastorm-engine.mjs +19 -0
- package/test/manual-omastorm-shaders.mjs +28 -0
- package/test/manual-omastorm-software.mjs +25 -0
- package/test/manual-pointer-scroll.html +43 -0
- package/test/manual-qml-removal.js +46 -0
- package/test/manual-without-qml.js +45 -0
- package/test/pointer-drag.test.js +104 -0
- package/test/qml-canvas-dom.test.js +37 -0
- package/test/qml-canvas.test.js +21 -0
- package/test/qml-clipboard-bridge.test.js +25 -0
- package/test/qml-clipboard-wayland.test.js +40 -0
- package/test/qml-connections-timing.test.js +95 -0
- package/test/qml-controls-window.test.js +182 -0
- package/test/qml-dynamic.test.js +401 -0
- package/test/qml-flea-app.test.js +146 -0
- package/test/qml-flea-backend.test.js +78 -0
- package/test/qml-flea-columns-backend.test.js +38 -0
- package/test/qml-flea-compile.test.js +78 -0
- package/test/qml-flea-launcher.test.js +88 -0
- package/test/qml-flea-markdown-backend.test.js +31 -0
- package/test/qml-flea-media.test.js +28 -0
- package/test/qml-flea-navigation-backend.test.js +35 -0
- package/test/qml-flea-navigation-timing.test.js +147 -0
- package/test/qml-flea-preview-backend.test.js +53 -0
- package/test/qml-flea-previews.test.js +163 -0
- package/test/qml-flea-rows.test.js +101 -0
- package/test/qml-flea-textures.test.js +59 -0
- package/test/qml-flea-view-switch.test.js +33 -0
- package/test/qml-flea-wayland.test.js +68 -0
- package/test/qml-flea.test.js +34 -0
- package/test/qml-fonts.test.js +31 -0
- package/test/qml-grid-multiline.test.js +48 -0
- package/test/qml-host.test.js +238 -0
- package/test/qml-imports.test.js +102 -0
- package/test/qml-inherited-handlers.test.js +51 -0
- package/test/qml-input-selection.test.js +66 -0
- package/test/qml-interactions.test.js +309 -0
- package/test/qml-language.test.js +167 -0
- package/test/qml-launcher.test.js +54 -0
- package/test/qml-layout-text.test.js +205 -0
- package/test/qml-locale.test.js +45 -0
- package/test/qml-media.test.js +63 -0
- package/test/qml-nonvisual-visible.test.js +19 -0
- package/test/qml-persisted-delegates.test.js +45 -0
- package/test/qml-quickshell-controls.test.js +65 -0
- package/test/qml-quickshell-settings.test.js +62 -0
- package/test/qml-repeater.test.js +72 -0
- package/test/qml-shader-runtime.test.js +6 -0
- package/test/qml-shortcuts.test.js +39 -0
- package/test/qml-socket.test.js +60 -0
- package/test/qml-software-shader.test.js +55 -0
- package/test/qml-style-updates.test.js +124 -0
- package/test/qml-textures.test.js +66 -0
- package/test/qml-views.test.js +332 -0
- package/test/qml-window-update.test.js +36 -0
- package/test/qml-worker.test.js +69 -0
- package/test/qml.test.js +83 -0
- package/test/render-interval.test.js +33 -0
- package/test/renderer-host.test.js +11 -0
- package/test/text-input-pointer.test.js +31 -0
- package/test/text-input-scroll.test.js +29 -0
- package/test/text-input.test.js +47 -0
- package/test/title-no-paint.test.js +6 -0
- package/test/wayland-frontend.test.js +83 -0
- package/test/wayland-scroll.test.js +40 -0
- package/test/wayland-urlbar.test.js +61 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
2
|
+
import { resolve, join } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
export function parseArgs(args, env = process.env) {
|
|
6
|
+
let path = env.QS_CONFIG_PATH || '',
|
|
7
|
+
config = env.QS_CONFIG_NAME || 'default';
|
|
8
|
+
let help = false;
|
|
9
|
+
for (let i = 0; i < args.length; i++) {
|
|
10
|
+
const arg = args[i];
|
|
11
|
+
const next = () => {
|
|
12
|
+
if (!args[i + 1] || args[i + 1].startsWith('-'))
|
|
13
|
+
throw Error(`${arg} needs a value`);
|
|
14
|
+
return args[++i];
|
|
15
|
+
};
|
|
16
|
+
if (arg === '-h' || arg === '--help') help = true;
|
|
17
|
+
else if (arg === '-p' || arg === '--path') path = next();
|
|
18
|
+
else if (arg.startsWith('--path=')) path = arg.slice(7);
|
|
19
|
+
else if (arg === '-c' || arg === '--config') {
|
|
20
|
+
config = next();
|
|
21
|
+
path = '';
|
|
22
|
+
} else throw Error(`Unsupported Quickshell option: ${arg}`);
|
|
23
|
+
}
|
|
24
|
+
if (help) return { help: true };
|
|
25
|
+
if (!path) {
|
|
26
|
+
if (!/^[\w.-]+$/.test(config) || config === '..')
|
|
27
|
+
throw Error('Invalid configuration name');
|
|
28
|
+
const base = env.XDG_CONFIG_HOME || (env.HOME && join(env.HOME, '.config'));
|
|
29
|
+
if (!base) throw Error('Set --path, XDG_CONFIG_HOME or HOME');
|
|
30
|
+
path = join(
|
|
31
|
+
base,
|
|
32
|
+
'quickshell',
|
|
33
|
+
...(config === 'default' ? [] : [config]),
|
|
34
|
+
'shell.qml',
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
path = resolve(path);
|
|
38
|
+
if (statSync(path).isDirectory()) path = join(path, 'shell.qml');
|
|
39
|
+
if (!statSync(path).isFile()) throw Error(`Not a QML file: ${path}`);
|
|
40
|
+
return { path };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function main(args) {
|
|
44
|
+
const options = parseArgs(args);
|
|
45
|
+
if (options.help) {
|
|
46
|
+
console.log(
|
|
47
|
+
`Usage: qs -p FILE_OR_DIRECTORY\n qs -c CONFIG\n\nExperimental Buninu QML/Wayland frontend, not the native Quickshell runtime.\nUses WAYLAND_DISPLAY, XDG_RUNTIME_DIR and QML_IMPORT_PATH.\n-p, --path QML file or directory containing shell.qml\n-c, --config Name under $XDG_CONFIG_HOME/quickshell\n-h, --help Show this help\n\nOnly supported QML features run. Native Quickshell IPC, Qt plugins and\nmultiple native windows are not implemented. Unknown arguments fail explicitly.`,
|
|
48
|
+
);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const { launch } = await import('../../frontends/wayland/index.js');
|
|
52
|
+
const app = await launch({
|
|
53
|
+
chrome: false,
|
|
54
|
+
url: pathToFileURL(options.path).href,
|
|
55
|
+
width: 900,
|
|
56
|
+
height: 600,
|
|
57
|
+
});
|
|
58
|
+
const stop = () => app.close();
|
|
59
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP'])
|
|
60
|
+
process.on(signal, stop);
|
|
61
|
+
try {
|
|
62
|
+
await app.done;
|
|
63
|
+
} finally {
|
|
64
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP'])
|
|
65
|
+
process.off(signal, stop);
|
|
66
|
+
app.close();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Package entry adapters share QS configuration resolution with the standalone qs.
|
|
71
|
+
export function backendArgs(args, env = process.env) {
|
|
72
|
+
const separator = args.indexOf('--');
|
|
73
|
+
const options = parseArgs(separator < 0 ? args : args.slice(0, separator), env);
|
|
74
|
+
if (options.help) return options;
|
|
75
|
+
return { args: [pathToFileURL(options.path).href, ...(separator < 0 ? [] : args.slice(separator + 1))] };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function backendUsage(backend) {
|
|
79
|
+
return `Usage: qs${backend} [-p FILE_OR_DIRECTORY | -c CONFIG] [-- BACKEND_ARGS...]
|
|
80
|
+
buninu-browser --quickshell-${backend} [-p FILE_OR_DIRECTORY | -c CONFIG] [-- BACKEND_ARGS...]
|
|
81
|
+
|
|
82
|
+
Experimental Buninu QML runtime using the ${backend} backend, not native Quickshell.
|
|
83
|
+
-p, --path QML file or directory containing shell.qml
|
|
84
|
+
-c, --config Name under $XDG_CONFIG_HOME/quickshell
|
|
85
|
+
-h, --help Show this help
|
|
86
|
+
Uses QS_CONFIG_PATH, QS_CONFIG_NAME and QML_IMPORT_PATH.
|
|
87
|
+
Without -p/-c, uses the same default configuration as standalone qs.
|
|
88
|
+
Arguments after -- are forwarded unchanged to ${backend}.
|
|
89
|
+
For casty, another -- separates Casty options from browser engine options.
|
|
90
|
+
Only supported QML features run; native Quickshell IPC and Qt plugins are not implemented.`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Independent executables and the package routes use the same backend adapter.
|
|
94
|
+
export async function runBackend(backend, args = Bun.argv.slice(2)) {
|
|
95
|
+
try {
|
|
96
|
+
if (!['wayland', 'headless', 'casty'].includes(backend))
|
|
97
|
+
throw Error(`Unknown Quickshell backend: ${backend}`);
|
|
98
|
+
const forwarded = backendArgs(args);
|
|
99
|
+
if (forwarded.help) {
|
|
100
|
+
console.log(backendUsage(backend));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const engine = resolve(import.meta.dir, '../../buninu-browser.js');
|
|
104
|
+
let cmd;
|
|
105
|
+
if (backend === 'casty') {
|
|
106
|
+
const { castyCommand } = await import('../../index.js');
|
|
107
|
+
cmd = castyCommand(forwarded.args);
|
|
108
|
+
} else {
|
|
109
|
+
cmd = backend === 'wayland'
|
|
110
|
+
? [process.execPath, resolve(import.meta.dir, '../../frontends/wayland/index.js'), '--window-size=900,600', ...forwarded.args]
|
|
111
|
+
: [process.execPath, engine, ...forwarded.args];
|
|
112
|
+
}
|
|
113
|
+
const child = Bun.spawn({
|
|
114
|
+
cmd,
|
|
115
|
+
env: backend === 'casty' ? { ...process.env, CASTY_BROWSER: engine } : process.env,
|
|
116
|
+
stdio: ['inherit', 'inherit', 'inherit'],
|
|
117
|
+
});
|
|
118
|
+
const handlers = ['SIGINT', 'SIGTERM', 'SIGHUP'].map(signal => {
|
|
119
|
+
const handler = () => child.kill(signal);
|
|
120
|
+
process.on(signal, handler);
|
|
121
|
+
return [signal, handler];
|
|
122
|
+
});
|
|
123
|
+
try {
|
|
124
|
+
process.exitCode = (await child.exited) ?? 1;
|
|
125
|
+
} finally {
|
|
126
|
+
for (const [signal, handler] of handlers) process.off(signal, handler);
|
|
127
|
+
}
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.error(`buninu qs${backend}: ${error.message}`);
|
|
130
|
+
process.exitCode = 1;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
// Resolution only: source is fetched via the existing renderer resource bridge.
|
|
2
|
+
// No filesystem access, host evaluation, native plugins, or persistent global cache.
|
|
3
|
+
import { parseQml } from './parser.js';
|
|
4
|
+
import { parse } from '../../vendor/acorn/dist/acorn.mjs';
|
|
5
|
+
export const builtinTypes = new Set([
|
|
6
|
+
'MediaPlayer', 'AudioOutput', 'VideoOutput', 'IdleInhibitor',
|
|
7
|
+
'Canvas',
|
|
8
|
+
'ShaderEffect',
|
|
9
|
+
'ShaderEffectSource',
|
|
10
|
+
'LinearGradient',
|
|
11
|
+
'XmlListModel',
|
|
12
|
+
'XmlListModelRole',
|
|
13
|
+
'WorkerScript',
|
|
14
|
+
'LoggingCategory',
|
|
15
|
+
'Flow',
|
|
16
|
+
'GridLayout', 'RowLayout',
|
|
17
|
+
'ColumnLayout',
|
|
18
|
+
'ListView',
|
|
19
|
+
'GridView',
|
|
20
|
+
'ListModel',
|
|
21
|
+
'DelegateModel',
|
|
22
|
+
'DelegateModelGroup',
|
|
23
|
+
'PointHandler',
|
|
24
|
+
'Binding',
|
|
25
|
+
'Gradient',
|
|
26
|
+
'GradientStop',
|
|
27
|
+
'PathMultiline', 'PathRectangle',
|
|
28
|
+
'IpcHandler',
|
|
29
|
+
'DropArea',
|
|
30
|
+
'DragHandler',
|
|
31
|
+
'FrameAnimation',
|
|
32
|
+
'Image',
|
|
33
|
+
'Flickable',
|
|
34
|
+
'TextInput',
|
|
35
|
+
'TextEdit',
|
|
36
|
+
'Item',
|
|
37
|
+
'Rectangle',
|
|
38
|
+
'Text',
|
|
39
|
+
'MouseArea',
|
|
40
|
+
'FloatingWindow',
|
|
41
|
+
'Column',
|
|
42
|
+
'Label',
|
|
43
|
+
'TextField',
|
|
44
|
+
'Button',
|
|
45
|
+
'QtObject',
|
|
46
|
+
'Singleton',
|
|
47
|
+
'TextMetrics',
|
|
48
|
+
'FontMetrics',
|
|
49
|
+
'Timer',
|
|
50
|
+
'Connections',
|
|
51
|
+
'FileView',
|
|
52
|
+
'Socket',
|
|
53
|
+
'Process',
|
|
54
|
+
'StdioCollector',
|
|
55
|
+
'SplitParser',
|
|
56
|
+
'Shortcut', 'Instantiator', 'Repeater',
|
|
57
|
+
'TapHandler',
|
|
58
|
+
'HoverHandler',
|
|
59
|
+
'Row',
|
|
60
|
+
'Grid',
|
|
61
|
+
'Shape',
|
|
62
|
+
'ShapePath',
|
|
63
|
+
'PathSvg',
|
|
64
|
+
'Scale',
|
|
65
|
+
'Translate',
|
|
66
|
+
'FocusScope',
|
|
67
|
+
'ShellRoot',
|
|
68
|
+
'Loader',
|
|
69
|
+
'Component',
|
|
70
|
+
'Behavior',
|
|
71
|
+
'NumberAnimation',
|
|
72
|
+
'PropertyAnimation',
|
|
73
|
+
'PauseAnimation',
|
|
74
|
+
'SequentialAnimation',
|
|
75
|
+
'ParallelAnimation',
|
|
76
|
+
'ScriptAction',
|
|
77
|
+
'PropertyAction',
|
|
78
|
+
]);
|
|
79
|
+
const builtinModules = new Set([
|
|
80
|
+
'QtQml.XmlListModel',
|
|
81
|
+
'QtQml',
|
|
82
|
+
'QtQml.Models',
|
|
83
|
+
'QtMultimedia',
|
|
84
|
+
'Quickshell.Wayland',
|
|
85
|
+
'QtQuick',
|
|
86
|
+
'QtQuick.Window',
|
|
87
|
+
'QtQuick.Controls',
|
|
88
|
+
'QtQuick.Shapes',
|
|
89
|
+
'QtQuick.Layouts',
|
|
90
|
+
'Quickshell',
|
|
91
|
+
'Quickshell.Io',
|
|
92
|
+
'Quickshell.Hyprland',
|
|
93
|
+
]);
|
|
94
|
+
const safeName = /^[A-Za-z_]\w*$/;
|
|
95
|
+
export function parseImport(line) {
|
|
96
|
+
const m =
|
|
97
|
+
/^(?:"([^"]+)"|([A-Za-z_]\w*(?:\.\w+)*))(?:\s+(\d+(?:\.\d+)?))?(?:\s+as\s+([A-Za-z_]\w*))?$/.exec(
|
|
98
|
+
line,
|
|
99
|
+
);
|
|
100
|
+
if (!m) throw Error(`Unsupported QML import: ${line}`);
|
|
101
|
+
return { path: m[1], module: m[2], version: m[3], alias: m[4] };
|
|
102
|
+
}
|
|
103
|
+
export function parseQmldir(source, url, version) {
|
|
104
|
+
const entries = new Map();
|
|
105
|
+
let module = null;
|
|
106
|
+
for (const raw of source.split(/\r?\n/)) {
|
|
107
|
+
const line = raw.replace(/#.*/, '').trim();
|
|
108
|
+
if (!line) continue;
|
|
109
|
+
const parts = line.split(/\s+/);
|
|
110
|
+
if (parts[0] === 'module') {
|
|
111
|
+
module = parts[1];
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (['typeinfo', 'designersupported'].includes(parts[0])) continue;
|
|
115
|
+
if (
|
|
116
|
+
[
|
|
117
|
+
'plugin',
|
|
118
|
+
'optional',
|
|
119
|
+
'classname',
|
|
120
|
+
'depends',
|
|
121
|
+
'import',
|
|
122
|
+
'prefer',
|
|
123
|
+
].includes(parts[0])
|
|
124
|
+
)
|
|
125
|
+
throw Error(`Unsupported qmldir directive at ${url}: ${line}`);
|
|
126
|
+
const singleton = parts[0] === 'singleton',
|
|
127
|
+
internal = parts[0] === 'internal';
|
|
128
|
+
if (singleton || internal) parts.shift();
|
|
129
|
+
const [name, ...rest] = parts;
|
|
130
|
+
if (!safeName.test(name)) throw Error(`Invalid qmldir entry: ${line}`);
|
|
131
|
+
const v = /^\d+\.\d+$/.test(rest[0]) ? rest.shift() : null;
|
|
132
|
+
if (rest.length !== 1 || !rest[0].endsWith('.qml'))
|
|
133
|
+
throw Error(`Unsupported qmldir entry: ${line}`);
|
|
134
|
+
const [major, minor] = (v || '0.0').split('.').map(Number),
|
|
135
|
+
[wantedMajor, wantedMinor] = (version || '').split('.').map(Number);
|
|
136
|
+
if (version && v && (major !== wantedMajor || minor > (wantedMinor || 0)))
|
|
137
|
+
continue;
|
|
138
|
+
const old = entries.get(name),
|
|
139
|
+
rank = major * 100000 + minor;
|
|
140
|
+
if (!old || rank > old.rank)
|
|
141
|
+
entries.set(name, {
|
|
142
|
+
url: new URL(rest[0], url).href,
|
|
143
|
+
singleton,
|
|
144
|
+
internal,
|
|
145
|
+
rank,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return { entries, module };
|
|
149
|
+
}
|
|
150
|
+
function* graphSteps(source, url, fetchResource, { importRoots = [] } = {}) {
|
|
151
|
+
const documents = new Map(),
|
|
152
|
+
scripts = new Map(),
|
|
153
|
+
reads = new Map();
|
|
154
|
+
let bytes = 0;
|
|
155
|
+
const base = new URL(url),
|
|
156
|
+
resourceLimit = base.protocol === 'file:' ? 1024 : 128;
|
|
157
|
+
const graph = { entry: url, documents, scripts };
|
|
158
|
+
function allowed(href) {
|
|
159
|
+
const u = new URL(href);
|
|
160
|
+
if (!['file:', 'http:', 'https:'].includes(u.protocol))
|
|
161
|
+
throw Error(`Unsupported QML resource URL: ${href}`);
|
|
162
|
+
if (base.protocol !== 'file:' && u.protocol === 'file:')
|
|
163
|
+
throw Error('Remote QML cannot import local files');
|
|
164
|
+
return u.href;
|
|
165
|
+
}
|
|
166
|
+
function* read(href, optional = false, kind = 'metadata') {
|
|
167
|
+
href = allowed(href);
|
|
168
|
+
if (!reads.has(href)) {
|
|
169
|
+
if (reads.size >= resourceLimit)
|
|
170
|
+
throw Error('QML import resource limit (' + resourceLimit + ')');
|
|
171
|
+
let response;
|
|
172
|
+
try {
|
|
173
|
+
response = yield { href, kind };
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (/ENOENT|not found|404/i.test(error.message))
|
|
176
|
+
response = { status: 404 };
|
|
177
|
+
else throw error;
|
|
178
|
+
}
|
|
179
|
+
let text = null;
|
|
180
|
+
if (response.status !== 404) {
|
|
181
|
+
if (response.ok === false)
|
|
182
|
+
throw Error(`QML import failed (${response.status}): ${href}`);
|
|
183
|
+
if (response.url) allowed(response.url);
|
|
184
|
+
const body = response.body ?? new Uint8Array();
|
|
185
|
+
bytes += body.byteLength;
|
|
186
|
+
if (body.byteLength > 256 * 1024 || bytes > 4 * 1024 * 1024)
|
|
187
|
+
throw Error('QML import byte limit');
|
|
188
|
+
text = new TextDecoder().decode(body);
|
|
189
|
+
}
|
|
190
|
+
reads.set(href, text);
|
|
191
|
+
}
|
|
192
|
+
const text = reads.get(href);
|
|
193
|
+
if (text === null && !optional)
|
|
194
|
+
throw Error(`QML resource not found: ${href}`);
|
|
195
|
+
return text;
|
|
196
|
+
}
|
|
197
|
+
function* script(href, chain = []) {
|
|
198
|
+
if (chain.includes(href))
|
|
199
|
+
throw Error(`Cyclic JS import: ${[...chain, href].join(' -> ')}`);
|
|
200
|
+
if (scripts.has(href)) return;
|
|
201
|
+
let text = yield* read(href, false, 'script');
|
|
202
|
+
const deps = [];
|
|
203
|
+
let library = false;
|
|
204
|
+
text = text.replace(
|
|
205
|
+
/^\s*\.(pragma\s+library|import\s+[^\n]+)\s*$/gm,
|
|
206
|
+
(_all, directive) => {
|
|
207
|
+
if (directive === 'pragma library') library = true;
|
|
208
|
+
else {
|
|
209
|
+
const imp = parseImport(directive.slice(7).trim());
|
|
210
|
+
if (!imp.path?.endsWith('.js') || !imp.alias)
|
|
211
|
+
throw Error(`Unsupported JS import in ${href}`);
|
|
212
|
+
deps.push({ alias: imp.alias, url: new URL(imp.path, href).href });
|
|
213
|
+
}
|
|
214
|
+
return '';
|
|
215
|
+
},
|
|
216
|
+
);
|
|
217
|
+
const ast = parse(text, { ecmaVersion: 'latest' }),
|
|
218
|
+
exports = [];
|
|
219
|
+
for (const node of ast.body) {
|
|
220
|
+
if (node.type === 'FunctionDeclaration')
|
|
221
|
+
exports.push({ name: node.id.name, writable: false });
|
|
222
|
+
if (node.type === 'VariableDeclaration')
|
|
223
|
+
for (const d of node.declarations) {
|
|
224
|
+
if (d.id.type !== 'Identifier')
|
|
225
|
+
throw Error(`Unsupported JS export pattern in ${href}`);
|
|
226
|
+
exports.push({ name: d.id.name, writable: node.kind !== 'const' });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
scripts.set(href, { source: text, ast, exports, deps, library });
|
|
230
|
+
for (const d of deps) yield* script(d.url, [...chain, href]);
|
|
231
|
+
}
|
|
232
|
+
function* document(href, text, depth = 0) {
|
|
233
|
+
if (depth > 48) throw Error('QML import depth limit');
|
|
234
|
+
if (documents.has(href)) return documents.get(href);
|
|
235
|
+
let ast;
|
|
236
|
+
try {
|
|
237
|
+
ast = parseQml(text ?? (yield* read(href)));
|
|
238
|
+
} catch (error) {
|
|
239
|
+
throw Error(`QML load ${href}: ${error.message}`);
|
|
240
|
+
}
|
|
241
|
+
for (const pragma of ast.pragmas)
|
|
242
|
+
if (!['Singleton', 'ComponentBehavior: Bound'].includes(pragma))
|
|
243
|
+
throw Error(`Unsupported QML pragma in ${href}: ${pragma}`);
|
|
244
|
+
const doc = {
|
|
245
|
+
url: href,
|
|
246
|
+
ast,
|
|
247
|
+
types: new Map(),
|
|
248
|
+
values: new Map(),
|
|
249
|
+
builtins: new Map(),
|
|
250
|
+
inlineScopes: new WeakMap(),
|
|
251
|
+
};
|
|
252
|
+
documents.set(href, doc);
|
|
253
|
+
const dirs = [];
|
|
254
|
+
const hasBase = ['http:', 'https:', 'file:'].includes(
|
|
255
|
+
new URL(href).protocol,
|
|
256
|
+
);
|
|
257
|
+
function* directory(dir, alias, version, required = false, module) {
|
|
258
|
+
const qurl = new URL('qmldir', dir).href,
|
|
259
|
+
body = yield* read(qurl, !required);
|
|
260
|
+
const meta =
|
|
261
|
+
body === null
|
|
262
|
+
? { entries: new Map() }
|
|
263
|
+
: parseQmldir(body, qurl, version);
|
|
264
|
+
if (module && meta.module !== module)
|
|
265
|
+
throw Error(`QML module mismatch: expected ${module} at ${qurl}`);
|
|
266
|
+
dirs.push({ dir, alias, entries: meta.entries, implicit: false });
|
|
267
|
+
}
|
|
268
|
+
const knownGlobals = new Set([
|
|
269
|
+
'Flow',
|
|
270
|
+
'LoggingCategory',
|
|
271
|
+
'XmlListModel',
|
|
272
|
+
'ListView',
|
|
273
|
+
'GridView',
|
|
274
|
+
'Binding',
|
|
275
|
+
'Gradient',
|
|
276
|
+
'PointerHandler',
|
|
277
|
+
'Drag',
|
|
278
|
+
'Flickable',
|
|
279
|
+
'Image',
|
|
280
|
+
'TextInput',
|
|
281
|
+
'TextEdit',
|
|
282
|
+
'Application',
|
|
283
|
+
'Easing',
|
|
284
|
+
'Animation',
|
|
285
|
+
'Text',
|
|
286
|
+
'Behavior',
|
|
287
|
+
'NumberAnimation',
|
|
288
|
+
'PropertyAnimation',
|
|
289
|
+
'PauseAnimation',
|
|
290
|
+
'SequentialAnimation',
|
|
291
|
+
'ParallelAnimation',
|
|
292
|
+
'ScriptAction',
|
|
293
|
+
'PropertyAction',
|
|
294
|
+
'Component',
|
|
295
|
+
'Loader',
|
|
296
|
+
'Font',
|
|
297
|
+
'Accessible',
|
|
298
|
+
'Shape',
|
|
299
|
+
'ShapePath',
|
|
300
|
+
'Qt',
|
|
301
|
+
'TapHandler',
|
|
302
|
+
'Quickshell',
|
|
303
|
+
'Hyprland',
|
|
304
|
+
'FileViewError',
|
|
305
|
+
'Math',
|
|
306
|
+
'Number',
|
|
307
|
+
'String',
|
|
308
|
+
'Boolean',
|
|
309
|
+
'Array',
|
|
310
|
+
'Object',
|
|
311
|
+
'JSON',
|
|
312
|
+
'Date',
|
|
313
|
+
'RegExp',
|
|
314
|
+
'Map',
|
|
315
|
+
'Set',
|
|
316
|
+
'Promise',
|
|
317
|
+
'Error',
|
|
318
|
+
'TypeError',
|
|
319
|
+
'Infinity',
|
|
320
|
+
'NaN',
|
|
321
|
+
]);
|
|
322
|
+
let needsLocal = false;
|
|
323
|
+
const scan = (node, parent, key) => {
|
|
324
|
+
if (!node || typeof node !== 'object') return;
|
|
325
|
+
if (
|
|
326
|
+
node.type === 'Identifier' &&
|
|
327
|
+
/^[A-Z]/.test(node.name) &&
|
|
328
|
+
!knownGlobals.has(node.name) &&
|
|
329
|
+
!(
|
|
330
|
+
parent?.type === 'MemberExpression' &&
|
|
331
|
+
key === 'property' &&
|
|
332
|
+
!parent.computed
|
|
333
|
+
) &&
|
|
334
|
+
!(parent?.type === 'Property' && key === 'key')
|
|
335
|
+
)
|
|
336
|
+
needsLocal = true;
|
|
337
|
+
for (const [k, v] of Object.entries(node))
|
|
338
|
+
if (v && typeof v === 'object') {
|
|
339
|
+
if (Array.isArray(v)) v.forEach((n) => scan(n, node, k));
|
|
340
|
+
else scan(v, node, k);
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
const collect = (o, inherited = new Map()) => {
|
|
344
|
+
const scope = new Map(inherited);
|
|
345
|
+
for (const c of o.components ?? []) {
|
|
346
|
+
if (scope.has(c.name) && !inherited.has(c.name))
|
|
347
|
+
throw Error('Duplicate inline component ' + c.name);
|
|
348
|
+
scope.set(c.name, c.object);
|
|
349
|
+
}
|
|
350
|
+
doc.inlineScopes.set(o, scope);
|
|
351
|
+
for (const c of o.components ?? []) collect(c.object, scope);
|
|
352
|
+
for (const p of o.properties) if (p.object) collect(p.object, scope);
|
|
353
|
+
for (const child of o.children) collect(child, scope);
|
|
354
|
+
};
|
|
355
|
+
collect(ast.root);
|
|
356
|
+
const inspect = (o) => {
|
|
357
|
+
if (!builtinTypes.has(o.type) && !o.type.includes('.')) needsLocal = true;
|
|
358
|
+
for (const p of o.properties) {
|
|
359
|
+
scan(p.ast);
|
|
360
|
+
if (p.object) inspect(p.object);
|
|
361
|
+
}
|
|
362
|
+
o.children.forEach(inspect);
|
|
363
|
+
for (const c of o.components ?? []) inspect(c.object);
|
|
364
|
+
};
|
|
365
|
+
inspect(ast.root);
|
|
366
|
+
if (hasBase && needsLocal) {
|
|
367
|
+
yield* directory(new URL('.', href).href, null);
|
|
368
|
+
dirs[0].implicit = true;
|
|
369
|
+
}
|
|
370
|
+
const aliases = new Set();
|
|
371
|
+
for (const raw of ast.imports) {
|
|
372
|
+
const imp = parseImport(raw);
|
|
373
|
+
if (imp.alias) {
|
|
374
|
+
if (aliases.has(imp.alias))
|
|
375
|
+
throw Error(`Duplicate QML import alias: ${imp.alias}`);
|
|
376
|
+
aliases.add(imp.alias);
|
|
377
|
+
}
|
|
378
|
+
if (imp.module && builtinModules.has(imp.module)) {
|
|
379
|
+
if (imp.alias) doc.builtins.set(imp.alias, imp.module);
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
if (imp.path?.endsWith('.js')) {
|
|
383
|
+
if (!imp.alias) throw Error(`JS import needs an alias: ${raw}`);
|
|
384
|
+
const target = new URL(imp.path, href).href;
|
|
385
|
+
yield* script(target);
|
|
386
|
+
doc.values.set(imp.alias, { kind: 'js', url: target });
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (!hasBase)
|
|
390
|
+
throw Error(`Relative imports require a file or HTTP URL: ${raw}`);
|
|
391
|
+
if (imp.path)
|
|
392
|
+
yield* directory(
|
|
393
|
+
new URL(imp.path.replace(/\/?$/, '/'), href).href,
|
|
394
|
+
imp.alias,
|
|
395
|
+
imp.version,
|
|
396
|
+
);
|
|
397
|
+
else {
|
|
398
|
+
const suffix = imp.module.replaceAll('.', '/') + '/';
|
|
399
|
+
const roots = [
|
|
400
|
+
...new Set([
|
|
401
|
+
new URL('.', href).href,
|
|
402
|
+
new URL('.', url).href,
|
|
403
|
+
...importRoots,
|
|
404
|
+
]),
|
|
405
|
+
];
|
|
406
|
+
let found = false;
|
|
407
|
+
for (const root of roots) {
|
|
408
|
+
const dir = new URL(suffix, root).href;
|
|
409
|
+
if ((yield* read(new URL('qmldir', dir).href, true)) !== null) {
|
|
410
|
+
yield* directory(dir, imp.alias, imp.version, true, imp.module);
|
|
411
|
+
found = true;
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (!found)
|
|
416
|
+
throw Error(`QML module not found: ${imp.module} (from ${href})`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const props = [],
|
|
420
|
+
objects = [];
|
|
421
|
+
const walk = (o) => {
|
|
422
|
+
objects.push(o);
|
|
423
|
+
props.push(...o.properties);
|
|
424
|
+
for (const p of o.properties) if (p.object) walk(p.object);
|
|
425
|
+
o.children.forEach(walk);
|
|
426
|
+
for (const c of o.components ?? []) walk(c.object);
|
|
427
|
+
};
|
|
428
|
+
walk(ast.root);
|
|
429
|
+
for (const o of objects) {
|
|
430
|
+
const parts = o.type.split('.');
|
|
431
|
+
if (builtinTypes.has(o.type) || doc.inlineScopes.get(o)?.has(o.type))
|
|
432
|
+
continue;
|
|
433
|
+
if (
|
|
434
|
+
parts.length === 2 &&
|
|
435
|
+
doc.builtins.has(parts[0]) &&
|
|
436
|
+
builtinTypes.has(parts[1])
|
|
437
|
+
) {
|
|
438
|
+
doc.types.set(o.type, { builtin: parts[1] });
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
const candidates = [];
|
|
442
|
+
for (const d of dirs) {
|
|
443
|
+
const name = d.alias
|
|
444
|
+
? parts[0] === d.alias && parts.length === 2
|
|
445
|
+
? parts[1]
|
|
446
|
+
: null
|
|
447
|
+
: parts.length === 1
|
|
448
|
+
? parts[0]
|
|
449
|
+
: null;
|
|
450
|
+
if (!name) continue;
|
|
451
|
+
const entry = d.entries.get(name);
|
|
452
|
+
if (entry) {
|
|
453
|
+
if (!entry.internal || d.implicit) candidates.push(entry);
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
const target = new URL(`${name}.qml`, d.dir).href;
|
|
457
|
+
if ((yield* read(target, true)) !== null)
|
|
458
|
+
candidates.push({ url: target, singleton: false });
|
|
459
|
+
}
|
|
460
|
+
const unique = [...new Map(candidates.map((e) => [e.url, e])).values()];
|
|
461
|
+
if (unique.length !== 1)
|
|
462
|
+
throw Error(
|
|
463
|
+
`${unique.length ? 'Ambiguous' : 'Unknown'} QML type ${o.type} in ${href}`,
|
|
464
|
+
);
|
|
465
|
+
const entry = unique[0];
|
|
466
|
+
if (entry.singleton)
|
|
467
|
+
throw Error(`Cannot instantiate singleton ${o.type}`);
|
|
468
|
+
doc.types.set(o.type, entry);
|
|
469
|
+
const child = yield* document(entry.url, null, depth + 1);
|
|
470
|
+
if (child.ast.pragmas.includes('Singleton'))
|
|
471
|
+
throw Error(`Singleton cannot be instantiated: ${entry.url}`);
|
|
472
|
+
}
|
|
473
|
+
const code = props.map((p) => p.code).join('\n');
|
|
474
|
+
for (const d of dirs)
|
|
475
|
+
for (const [name, entry] of d.entries) {
|
|
476
|
+
if (!entry.singleton || (entry.internal && !d.implicit)) continue;
|
|
477
|
+
const key = d.alias ? `${d.alias}.${name}` : name;
|
|
478
|
+
if (!new RegExp(`\\b${key.replaceAll('.', '\\.')}\\b`).test(code))
|
|
479
|
+
continue;
|
|
480
|
+
const old = doc.values.get(key);
|
|
481
|
+
if (old && old.url !== entry.url)
|
|
482
|
+
throw Error(`Ambiguous QML singleton ${key}`);
|
|
483
|
+
doc.values.set(key, { kind: 'singleton', url: entry.url });
|
|
484
|
+
const target = yield* document(entry.url, null, depth + 1);
|
|
485
|
+
if (!target.ast.pragmas.includes('Singleton'))
|
|
486
|
+
throw Error(`qmldir singleton needs pragma Singleton: ${entry.url}`);
|
|
487
|
+
}
|
|
488
|
+
return doc;
|
|
489
|
+
}
|
|
490
|
+
yield* document(url, source);
|
|
491
|
+
return graph;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** One resolver, driven asynchronously for web resources or synchronously for local application components. */
|
|
495
|
+
export async function loadQmlGraph(source, url, fetchResource, options = {}) {
|
|
496
|
+
const steps = graphSteps(source, url, null, options);
|
|
497
|
+
let next = steps.next();
|
|
498
|
+
while (!next.done) {
|
|
499
|
+
try {
|
|
500
|
+
next = steps.next(await fetchResource(next.value.href, next.value.kind));
|
|
501
|
+
} catch (error) {
|
|
502
|
+
next = steps.throw(error);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return next.value;
|
|
506
|
+
}
|
|
507
|
+
export function loadQmlGraphSync(source, url, fetchResource, options = {}) {
|
|
508
|
+
const steps = graphSteps(source, url, null, options);
|
|
509
|
+
let next = steps.next();
|
|
510
|
+
while (!next.done) {
|
|
511
|
+
try {
|
|
512
|
+
const response = fetchResource(next.value.href, next.value.kind);
|
|
513
|
+
if (response?.then)
|
|
514
|
+
throw Error('Synchronous QML imports require synchronous resources');
|
|
515
|
+
next = steps.next(response);
|
|
516
|
+
} catch (error) {
|
|
517
|
+
next = steps.throw(error);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return next.value;
|
|
521
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Local QML adapter to the existing HTML media engine. No decoder/output backend.
|
|
2
|
+
export function createQmlMediaController(element,api) {
|
|
3
|
+
let closed=false,version=0,gain=1;
|
|
4
|
+
const current=()=>!closed&&api.valid(element);
|
|
5
|
+
const release=()=>{
|
|
6
|
+
const state=api.state(element);
|
|
7
|
+
if(state){
|
|
8
|
+
const pending=state.loading;
|
|
9
|
+
state.document=null;
|
|
10
|
+
api.close(state);
|
|
11
|
+
// In-flight probing may finish after disposal. Release its late decoders
|
|
12
|
+
// without closing the replacement source's state.
|
|
13
|
+
pending?.then(()=>api.close(state),()=>api.close(state));
|
|
14
|
+
}
|
|
15
|
+
api.forget(element);
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
async source(url) {
|
|
19
|
+
const generation=++version;release();
|
|
20
|
+
if(!current())return;
|
|
21
|
+
element.setAttribute('src',url);
|
|
22
|
+
if(!url)return;
|
|
23
|
+
const state=api.ensure(element);
|
|
24
|
+
if(!state)throw Error('Media decoder unavailable');
|
|
25
|
+
try {
|
|
26
|
+
await api.load(state);
|
|
27
|
+
if(!current()||generation!==version)return;
|
|
28
|
+
await api.seek(element,0);
|
|
29
|
+
} catch(error) {if(current()&&generation===version)throw error;}
|
|
30
|
+
},
|
|
31
|
+
play:()=>current()?api.play(element):undefined,
|
|
32
|
+
pause:()=>{if(current())api.pause(element);},
|
|
33
|
+
seek:seconds=>current()?api.seek(element,seconds):undefined,
|
|
34
|
+
snapshot() {
|
|
35
|
+
const state=api.state(element),status=api.snapshot(element);
|
|
36
|
+
return {...status,hasAudio:!!state?.hasAudio,hasVideo:!!state?.hasVideo,
|
|
37
|
+
ready:!!state?.probed,error:api.error(element),buffering:!!state?.buffering};
|
|
38
|
+
},
|
|
39
|
+
volume(value) {gain=Math.max(0,Math.min(1,Number(value)||0));api.gains.set(element,gain);},
|
|
40
|
+
dispose(){if(closed)return;closed=true;version++;release();api.gains.delete(element);},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// PCM already belongs to the output chunk; do not copy or touch decoder history.
|
|
44
|
+
export function applyQmlMediaGain(pcm,gain) {
|
|
45
|
+
if(gain===undefined||gain===1)return;
|
|
46
|
+
if(gain===0){pcm.fill(0);return;}
|
|
47
|
+
const view=new DataView(pcm.buffer,pcm.byteOffset,pcm.byteLength);
|
|
48
|
+
for(let i=0;i+1<pcm.byteLength;i+=2)view.setInt16(i,Math.round(view.getInt16(i,true)*gain),true);
|
|
49
|
+
}
|