From cf9a943fba1be1a0367cc3671efade99ada3c292 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Sat, 9 Mar 2024 18:41:57 +0800 Subject: [PATCH 1/9] feat: add config for `events` property --- .../client-api/src/element-context.model.ts | 2 + packages/client-api/src/index.ts | 1 + packages/client-api/src/init-element.ts | 13 ++ .../src/user-config/get-script-manager.ts | 49 ++++++++ packages/client-api/src/user-config/index.ts | 1 + .../window/base-element-config.model.ts | 2 + .../window/element-events-config.model.ts | 119 ++++++++++++++++++ .../src/user-config/window/index.ts | 1 + .../src/app/template-element.component.ts | 19 ++- 9 files changed, 204 insertions(+), 3 deletions(-) create mode 100644 packages/client-api/src/user-config/get-script-manager.ts create mode 100644 packages/client-api/src/user-config/window/element-events-config.model.ts diff --git a/packages/client-api/src/element-context.model.ts b/packages/client-api/src/element-context.model.ts index 82076893..87d0bfbd 100644 --- a/packages/client-api/src/element-context.model.ts +++ b/packages/client-api/src/element-context.model.ts @@ -49,6 +49,8 @@ interface BaseElementContext { */ providers: P; + // TODO: Consider adding `scripts` record where keys are script paths. + /** * Initializes a child group or template element. * @internal diff --git a/packages/client-api/src/index.ts b/packages/client-api/src/index.ts index 450beeec..b111d0ca 100644 --- a/packages/client-api/src/index.ts +++ b/packages/client-api/src/index.ts @@ -1,5 +1,6 @@ export { createLogger, toCssSelector } from './utils'; export { + getScriptManager, getChildConfigs, type GlobalConfig, type WindowConfig, diff --git a/packages/client-api/src/init-element.ts b/packages/client-api/src/init-element.ts index 8d14f52e..c359ff4c 100644 --- a/packages/client-api/src/init-element.ts +++ b/packages/client-api/src/init-element.ts @@ -71,6 +71,7 @@ export async function initElement( // provider. setElementContext('parsedConfig', parsedConfig); + // Build the CSS for the element. runWithOwner(args.owner, () => { createEffect(async () => { if (parsedConfig.styles) { @@ -89,6 +90,18 @@ export async function initElement( }); }); + // Import the scripts for the element. + runWithOwner(args.owner, () => { + createEffect(() => { + if (parsedConfig.events) { + for (const event of parsedConfig.events) { + const split = event.fn_path.split('#'); + import(split[0]!).then(module => module[split[1]!]!); + } + } + }); + }); + async function initChildElement(id: string) { const childConfig = childConfigs.find( childConfig => childConfig.id === id, diff --git a/packages/client-api/src/user-config/get-script-manager.ts b/packages/client-api/src/user-config/get-script-manager.ts new file mode 100644 index 00000000..31298c56 --- /dev/null +++ b/packages/client-api/src/user-config/get-script-manager.ts @@ -0,0 +1,49 @@ +import { + type Owner, + createEffect, + createSignal, + runWithOwner, +} from 'solid-js'; +import { createStore } from 'solid-js/store'; + +import { createLogger } from '~/utils'; + +const logger = createLogger('script-manager'); + +const [modules, setModules] = createStore>>( + {}, +); + +/** + * Abstraction over loading and invoking user-defined scripts. + */ +export function getScriptManager() { + return { + loadScript, + callFn, + }; +} + +async function loadScript(path: string): Promise { + const importPromise = import(path); + setModules({ [path]: importPromise }); + return importPromise; +} + +async function callFn(fnPath: string): Promise { + const split = fnPath.split('#'); + const foundModule = modules[split[0]!]; + + if (!foundModule) { + throw new Error('Invalid function path'); + } + + return foundModule.then(m => { + const fn = m[split[1]!]; + if (!fn) { + throw new Error('Invalid function path'); + } + + return fn(); + }); +} diff --git a/packages/client-api/src/user-config/index.ts b/packages/client-api/src/user-config/index.ts index b7eb4e9e..51522251 100644 --- a/packages/client-api/src/user-config/index.ts +++ b/packages/client-api/src/user-config/index.ts @@ -1,4 +1,5 @@ export * from './get-parsed-element-config'; +export * from './get-script-manager'; export * from './get-style-builder'; export * from './get-user-config'; export * from './global-config.model'; diff --git a/packages/client-api/src/user-config/window/base-element-config.model.ts b/packages/client-api/src/user-config/window/base-element-config.model.ts index b40ad67f..f5ff4690 100644 --- a/packages/client-api/src/user-config/window/base-element-config.model.ts +++ b/packages/client-api/src/user-config/window/base-element-config.model.ts @@ -2,12 +2,14 @@ import { z } from 'zod'; import type { Prettify } from '~/utils'; import { ProvidersConfigSchema } from './providers-config.model'; +import { ElementEventsConfigSchema } from './element-events-config.model'; export const BaseElementConfigSchema = z.object({ id: z.string(), class_names: z.array(z.string()).default([]), styles: z.string().optional(), providers: ProvidersConfigSchema, + events: ElementEventsConfigSchema, }); /** Base config for windows, groups, and components. */ diff --git a/packages/client-api/src/user-config/window/element-events-config.model.ts b/packages/client-api/src/user-config/window/element-events-config.model.ts new file mode 100644 index 00000000..24d3434a --- /dev/null +++ b/packages/client-api/src/user-config/window/element-events-config.model.ts @@ -0,0 +1,119 @@ +import { z } from 'zod'; +import type { Prettify } from '~/utils'; + +/** + * All available events on HTML elements. + **/ +const HTML_EVENTS = [ + 'onclick', + 'onfullscreenchange', + 'onfullscreenerror', + 'onabort', + 'onanimationcancel', + 'onanimationend', + 'onanimationiteration', + 'onanimationstart', + 'onauxclick', + 'onbeforeinput', + 'onblur', + 'oncancel', + 'oncanplay', + 'oncanplaythrough', + 'onchange', + 'onclose', + 'oncontextmenu', + 'oncopy', + 'oncuechange', + 'oncut', + 'ondblclick', + 'ondrag', + 'ondragend', + 'ondragenter', + 'ondragleave', + 'ondragover', + 'ondragstart', + 'ondrop', + 'ondurationchange', + 'onemptied', + 'onended', + 'onerror', + 'onfocus', + 'onformdata', + 'ongotpointercapture', + 'oninput', + 'oninvalid', + 'onkeydown', + 'onkeypress', + 'onkeyup', + 'onload', + 'onloadeddata', + 'onloadedmetadata', + 'onloadstart', + 'onlostpointercapture', + 'onmousedown', + 'onmouseenter', + 'onmouseleave', + 'onmousemove', + 'onmouseout', + 'onmouseover', + 'onmouseup', + 'onpaste', + 'onpause', + 'onplay', + 'onplaying', + 'onpointercancel', + 'onpointerdown', + 'onpointerenter', + 'onpointerleave', + 'onpointermove', + 'onpointerout', + 'onpointerover', + 'onpointerup', + 'onprogress', + 'onratechange', + 'onreset', + 'onresize', + 'onscroll', + 'onscrollend', + 'onsecuritypolicyviolation', + 'onseeked', + 'onseeking', + 'onselect', + 'onselectionchange', + 'onselectstart', + 'onslotchange', + 'onstalled', + 'onsubmit', + 'onsuspend', + 'ontimeupdate', + 'ontoggle', + 'ontouchcancel', + 'ontouchend', + 'ontouchmove', + 'ontouchstart', + 'ontransitioncancel', + 'ontransitionend', + 'ontransitionrun', + 'ontransitionstart', + 'onvolumechange', + 'onwaiting', + 'onwebkitanimationend', + 'onwebkitanimationiteration', + 'onwebkitanimationstart', + 'onwebkittransitionend', + 'onwheel', +] as const; + +export const ElementEventsConfigSchema = z + .array( + z.object({ + type: z.enum(HTML_EVENTS), + fn_path: z.string(), + selector: z.string().optional(), + }), + ) + .default([]); + +export type ElementEventsConfig = Prettify< + z.infer +>; diff --git a/packages/client-api/src/user-config/window/index.ts b/packages/client-api/src/user-config/window/index.ts index a86bdddf..937e359a 100644 --- a/packages/client-api/src/user-config/window/index.ts +++ b/packages/client-api/src/user-config/window/index.ts @@ -1,4 +1,5 @@ export * from './base-element-config.model'; +export * from './element-events-config.model'; export * from './group-config.model'; export * from './provider-config.model'; export * from './provider-type.model'; diff --git a/packages/client/src/app/template-element.component.ts b/packages/client/src/app/template-element.component.ts index 0ed130a7..99808fbf 100644 --- a/packages/client/src/app/template-element.component.ts +++ b/packages/client/src/app/template-element.component.ts @@ -1,5 +1,10 @@ import { createEffect, onCleanup, onMount } from 'solid-js'; -import { type ElementContext, createLogger, toCssSelector } from 'zebar'; +import { + type ElementContext, + createLogger, + toCssSelector, + getScriptManager, +} from 'zebar'; export interface TemplateElementProps { context: ElementContext; @@ -8,13 +13,21 @@ export interface TemplateElementProps { export function TemplateElement(props: TemplateElementProps) { const config = props.context.parsedConfig; const logger = createLogger(`#${config.id}`); + const scriptManager = getScriptManager(); // Create element with ID. const element = createRootElement(); // Update the HTML element when the template changes. - // @ts-ignore - TODO - createEffect(() => (element.innerHTML = config.template)); + createEffect(() => { + // @ts-ignore - TODO + element.innerHTML = config.template; + config.events.forEach(event => { + element.addEventListener(event.type, () => + scriptManager.callFn(event.fn_path), + ); + }); + }); onMount(() => logger.debug('Mounted')); onCleanup(() => logger.debug('Cleanup')); From 5cf6656336955f95396bdad4278e100920b78f44 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Sat, 9 Mar 2024 19:00:25 +0800 Subject: [PATCH 2/9] feat: add tauri fs plugin; add permissions for reading files within $HOME --- packages/client-api/src/init-element.ts | 4 +++- .../src/user-config/get-script-manager.ts | 14 +++++++++++++- packages/desktop/Cargo.lock | 7 +++++++ packages/desktop/Cargo.toml | 5 ++++- packages/desktop/capabilities/main.json | 2 ++ packages/desktop/tauri.conf.json | 6 +++++- 6 files changed, 34 insertions(+), 4 deletions(-) diff --git a/packages/client-api/src/init-element.ts b/packages/client-api/src/init-element.ts index c359ff4c..9293f141 100644 --- a/packages/client-api/src/init-element.ts +++ b/packages/client-api/src/init-element.ts @@ -11,6 +11,7 @@ import { getParsedElementConfig, getChildConfigs, type GlobalConfig, + getScriptManager, } from './user-config'; import { getElementProviders } from './providers'; import type { ElementContext } from './element-context.model'; @@ -36,6 +37,7 @@ export async function initElement( ): Promise { try { const styleBuilder = getStyleBuilder(); + const scriptManager = getScriptManager(); const childConfigs = getChildConfigs(args.rawConfig); // Create partial element context; `providers` and `parsedConfig` are set later. @@ -96,7 +98,7 @@ export async function initElement( if (parsedConfig.events) { for (const event of parsedConfig.events) { const split = event.fn_path.split('#'); - import(split[0]!).then(module => module[split[1]!]!); + scriptManager.loadScript(split[0]!); } } }); diff --git a/packages/client-api/src/user-config/get-script-manager.ts b/packages/client-api/src/user-config/get-script-manager.ts index 31298c56..ffeebff0 100644 --- a/packages/client-api/src/user-config/get-script-manager.ts +++ b/packages/client-api/src/user-config/get-script-manager.ts @@ -1,3 +1,5 @@ +import { convertFileSrc } from '@tauri-apps/api/core'; +import { join, homeDir } from '@tauri-apps/api/path'; import { type Owner, createEffect, @@ -25,7 +27,17 @@ export function getScriptManager() { } async function loadScript(path: string): Promise { - const importPromise = import(path); + const scriptPath = await join(await homeDir(), '.glzr/zebar', path); + const scriptAssetPath = convertFileSrc(scriptPath); + console.log('path', scriptPath, scriptAssetPath); + const importPromise = import(scriptAssetPath); + + const imgPath = await join(await homeDir(), '.glzr/zebar', 'out.png'); + const imgAssetPath = convertFileSrc(imgPath); + const img = new Image(); + img.src = imgAssetPath; + document.body.appendChild(img); + setModules({ [path]: importPromise }); return importPromise; } diff --git a/packages/desktop/Cargo.lock b/packages/desktop/Cargo.lock index 833dfc00..78a4298b 100644 --- a/packages/desktop/Cargo.lock +++ b/packages/desktop/Cargo.lock @@ -1944,6 +1944,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + [[package]] name = "httparse" version = "1.9.4" @@ -4723,6 +4729,7 @@ dependencies = [ "gtk", "heck 0.5.0", "http 1.1.0", + "http-range", "jni", "libc", "log", diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 648ae615..64e24ca2 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -17,7 +17,10 @@ anyhow = "1" async-trait = "0.1" clap = { version = "4", features = ["derive"] } reqwest = { version = "0.11", features = ["json"] } -tauri = { version = "2.0.0-beta", features = ["macos-private-api"] } +tauri = { version = "2.0.0-beta", features = [ + "protocol-asset", + "macos-private-api", +] } tauri-plugin-dialog = "2.0.0-beta" tauri-plugin-http = "2.0.0-beta" tauri-plugin-shell = "2.0.0-beta" diff --git a/packages/desktop/capabilities/main.json b/packages/desktop/capabilities/main.json index 53953338..447cb5d6 100644 --- a/packages/desktop/capabilities/main.json +++ b/packages/desktop/capabilities/main.json @@ -7,6 +7,8 @@ "app:default", "dialog:allow-message", "event:default", + "path:allow-resolve-directory", + "path:default", "window:default", "window:allow-center", "window:allow-close", diff --git a/packages/desktop/tauri.conf.json b/packages/desktop/tauri.conf.json index 0e496a8f..569d87fb 100644 --- a/packages/desktop/tauri.conf.json +++ b/packages/desktop/tauri.conf.json @@ -34,7 +34,11 @@ "app": { "macOSPrivateApi": true, "security": { - "csp": null + "csp": "default-src 'self'; img-src 'self' asset: https://asset.localhost", + "assetProtocol": { + "enable": true, + "scope": ["**", "**/*"] + } } } } From ba119581ae33c2a7b369025f3dc0d7650bd6f22b Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Sat, 9 Mar 2024 21:08:24 +0800 Subject: [PATCH 3/9] feat: basic working example --- .../src/user-config/get-script-manager.ts | 16 +- .../window/element-events-config.model.ts | 194 +++++++++--------- packages/desktop/tauri.conf.json | 2 +- 3 files changed, 100 insertions(+), 112 deletions(-) diff --git a/packages/client-api/src/user-config/get-script-manager.ts b/packages/client-api/src/user-config/get-script-manager.ts index ffeebff0..bd1f9c1e 100644 --- a/packages/client-api/src/user-config/get-script-manager.ts +++ b/packages/client-api/src/user-config/get-script-manager.ts @@ -1,11 +1,5 @@ import { convertFileSrc } from '@tauri-apps/api/core'; import { join, homeDir } from '@tauri-apps/api/path'; -import { - type Owner, - createEffect, - createSignal, - runWithOwner, -} from 'solid-js'; import { createStore } from 'solid-js/store'; import { createLogger } from '~/utils'; @@ -29,15 +23,8 @@ export function getScriptManager() { async function loadScript(path: string): Promise { const scriptPath = await join(await homeDir(), '.glzr/zebar', path); const scriptAssetPath = convertFileSrc(scriptPath); - console.log('path', scriptPath, scriptAssetPath); - const importPromise = import(scriptAssetPath); - - const imgPath = await join(await homeDir(), '.glzr/zebar', 'out.png'); - const imgAssetPath = convertFileSrc(imgPath); - const img = new Image(); - img.src = imgAssetPath; - document.body.appendChild(img); + const importPromise = import(scriptAssetPath); setModules({ [path]: importPromise }); return importPromise; } @@ -52,6 +39,7 @@ async function callFn(fnPath: string): Promise { return foundModule.then(m => { const fn = m[split[1]!]; + if (!fn) { throw new Error('Invalid function path'); } diff --git a/packages/client-api/src/user-config/window/element-events-config.model.ts b/packages/client-api/src/user-config/window/element-events-config.model.ts index 24d3434a..cc6c71c0 100644 --- a/packages/client-api/src/user-config/window/element-events-config.model.ts +++ b/packages/client-api/src/user-config/window/element-events-config.model.ts @@ -5,103 +5,103 @@ import type { Prettify } from '~/utils'; * All available events on HTML elements. **/ const HTML_EVENTS = [ - 'onclick', - 'onfullscreenchange', - 'onfullscreenerror', - 'onabort', - 'onanimationcancel', - 'onanimationend', - 'onanimationiteration', - 'onanimationstart', - 'onauxclick', - 'onbeforeinput', - 'onblur', - 'oncancel', - 'oncanplay', - 'oncanplaythrough', - 'onchange', - 'onclose', - 'oncontextmenu', - 'oncopy', - 'oncuechange', - 'oncut', - 'ondblclick', - 'ondrag', - 'ondragend', - 'ondragenter', - 'ondragleave', - 'ondragover', - 'ondragstart', - 'ondrop', - 'ondurationchange', - 'onemptied', - 'onended', - 'onerror', - 'onfocus', - 'onformdata', - 'ongotpointercapture', - 'oninput', - 'oninvalid', - 'onkeydown', - 'onkeypress', - 'onkeyup', - 'onload', - 'onloadeddata', - 'onloadedmetadata', - 'onloadstart', - 'onlostpointercapture', - 'onmousedown', - 'onmouseenter', - 'onmouseleave', - 'onmousemove', - 'onmouseout', - 'onmouseover', - 'onmouseup', - 'onpaste', - 'onpause', - 'onplay', - 'onplaying', - 'onpointercancel', - 'onpointerdown', - 'onpointerenter', - 'onpointerleave', - 'onpointermove', - 'onpointerout', - 'onpointerover', - 'onpointerup', - 'onprogress', - 'onratechange', - 'onreset', - 'onresize', - 'onscroll', - 'onscrollend', - 'onsecuritypolicyviolation', - 'onseeked', - 'onseeking', - 'onselect', - 'onselectionchange', - 'onselectstart', - 'onslotchange', - 'onstalled', - 'onsubmit', - 'onsuspend', - 'ontimeupdate', - 'ontoggle', - 'ontouchcancel', - 'ontouchend', - 'ontouchmove', - 'ontouchstart', - 'ontransitioncancel', - 'ontransitionend', - 'ontransitionrun', - 'ontransitionstart', - 'onvolumechange', - 'onwaiting', - 'onwebkitanimationend', - 'onwebkitanimationiteration', - 'onwebkitanimationstart', - 'onwebkittransitionend', - 'onwheel', + 'click', + 'fullscreenchange', + 'fullscreenerror', + 'abort', + 'animationcancel', + 'animationend', + 'animationiteration', + 'animationstart', + 'auxclick', + 'beforeinput', + 'blur', + 'cancel', + 'canplay', + 'canplaythrough', + 'change', + 'close', + 'contextmenu', + 'copy', + 'cuechange', + 'cut', + 'dblclick', + 'drag', + 'dragend', + 'dragenter', + 'dragleave', + 'dragover', + 'dragstart', + 'drop', + 'durationchange', + 'emptied', + 'ended', + 'error', + 'focus', + 'formdata', + 'gotpointercapture', + 'input', + 'invalid', + 'keydown', + 'keypress', + 'keyup', + 'load', + 'loadeddata', + 'loadedmetadata', + 'loadstart', + 'lostpointercapture', + 'mousedown', + 'mouseenter', + 'mouseleave', + 'mousemove', + 'mouseout', + 'mouseover', + 'mouseup', + 'paste', + 'pause', + 'play', + 'playing', + 'pointercancel', + 'pointerdown', + 'pointerenter', + 'pointerleave', + 'pointermove', + 'pointerout', + 'pointerover', + 'pointerup', + 'progress', + 'ratechange', + 'reset', + 'resize', + 'scroll', + 'scrollend', + 'securitypolicyviolation', + 'seeked', + 'seeking', + 'select', + 'selectionchange', + 'selectstart', + 'slotchange', + 'stalled', + 'submit', + 'suspend', + 'timeupdate', + 'toggle', + 'touchcancel', + 'touchend', + 'touchmove', + 'touchstart', + 'transitioncancel', + 'transitionend', + 'transitionrun', + 'transitionstart', + 'volumechange', + 'waiting', + 'webkitanimationend', + 'webkitanimationiteration', + 'webkitanimationstart', + 'webkittransitionend', + 'wheel', ] as const; export const ElementEventsConfigSchema = z diff --git a/packages/desktop/tauri.conf.json b/packages/desktop/tauri.conf.json index 569d87fb..11c3f476 100644 --- a/packages/desktop/tauri.conf.json +++ b/packages/desktop/tauri.conf.json @@ -37,7 +37,7 @@ "csp": "default-src 'self'; img-src 'self' asset: https://asset.localhost", "assetProtocol": { "enable": true, - "scope": ["**", "**/*"] + "scope": ["$HOME/.glzr/zebar/**"] } } } From ef5d39f16b2c25fb5ff94a5f17889cdb83773da3 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Sat, 9 Mar 2024 21:08:36 +0800 Subject: [PATCH 4/9] feat: clean up old event listeners; forward event and context to fn --- .../src/user-config/get-script-manager.ts | 9 ++++-- .../src/app/template-element.component.ts | 29 +++++++++++++++---- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/packages/client-api/src/user-config/get-script-manager.ts b/packages/client-api/src/user-config/get-script-manager.ts index bd1f9c1e..064eb233 100644 --- a/packages/client-api/src/user-config/get-script-manager.ts +++ b/packages/client-api/src/user-config/get-script-manager.ts @@ -3,6 +3,7 @@ import { join, homeDir } from '@tauri-apps/api/path'; import { createStore } from 'solid-js/store'; import { createLogger } from '~/utils'; +import type { ElementContext } from '../element-context.model'; const logger = createLogger('script-manager'); @@ -29,7 +30,11 @@ async function loadScript(path: string): Promise { return importPromise; } -async function callFn(fnPath: string): Promise { +async function callFn( + fnPath: string, + event: Event, + context: ElementContext, +): Promise { const split = fnPath.split('#'); const foundModule = modules[split[0]!]; @@ -44,6 +49,6 @@ async function callFn(fnPath: string): Promise { throw new Error('Invalid function path'); } - return fn(); + return fn(event, context); }); } diff --git a/packages/client/src/app/template-element.component.ts b/packages/client/src/app/template-element.component.ts index 99808fbf..ab5eeb9f 100644 --- a/packages/client/src/app/template-element.component.ts +++ b/packages/client/src/app/template-element.component.ts @@ -18,15 +18,16 @@ export function TemplateElement(props: TemplateElementProps) { // Create element with ID. const element = createRootElement(); + // Currently active event listeners. + let listeners: { type: string; fn: (event: Event) => Promise }[] = + []; + // Update the HTML element when the template changes. createEffect(() => { + clearEventListeners(); // @ts-ignore - TODO element.innerHTML = config.template; - config.events.forEach(event => { - element.addEventListener(event.type, () => - scriptManager.callFn(event.fn_path), - ); - }); + addEventListeners(); }); onMount(() => logger.debug('Mounted')); @@ -39,5 +40,23 @@ export function TemplateElement(props: TemplateElementProps) { return element; } + function clearEventListeners() { + listeners.forEach(({ type, fn }) => + element.removeEventListener(type, fn), + ); + + listeners = []; + } + + function addEventListeners() { + config.events.forEach(eventConfig => { + const callFn = (event: Event) => + scriptManager.callFn(eventConfig.fn_path, event, props.context); + + element.addEventListener(eventConfig.type, callFn); + listeners.push({ type: eventConfig.type, fn: callFn }); + }); + } + return element; } From 2cbbf3cc858f7895c8216e51ce9c52bffe851601 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Sun, 10 Mar 2024 14:08:39 +0800 Subject: [PATCH 5/9] feat: add script.js resource --- packages/desktop/resources/script.js | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 packages/desktop/resources/script.js diff --git a/packages/desktop/resources/script.js b/packages/desktop/resources/script.js new file mode 100644 index 00000000..1a2e6ee7 --- /dev/null +++ b/packages/desktop/resources/script.js @@ -0,0 +1,3 @@ +export function invokeMe(event, context) { + console.log('invokeMe', event, context); +} From 8a79976750de08381e3a5a94c4abbd1d001f8ee8 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Tue, 2 Jul 2024 17:04:53 +0800 Subject: [PATCH 6/9] feat: improve validation when loading scripts --- packages/client-api/src/init-element.ts | 18 +++-- .../src/user-config/get-script-manager.ts | 65 ++++++++++++++----- .../window/element-events-config.model.ts | 7 +- 3 files changed, 67 insertions(+), 23 deletions(-) diff --git a/packages/client-api/src/init-element.ts b/packages/client-api/src/init-element.ts index 9293f141..de1221ef 100644 --- a/packages/client-api/src/init-element.ts +++ b/packages/client-api/src/init-element.ts @@ -94,12 +94,18 @@ export async function initElement( // Import the scripts for the element. runWithOwner(args.owner, () => { - createEffect(() => { - if (parsedConfig.events) { - for (const event of parsedConfig.events) { - const split = event.fn_path.split('#'); - scriptManager.loadScript(split[0]!); - } + createEffect(async () => { + try { + await Promise.all( + parsedConfig.events + .map(config => config.fn_path) + .map(scriptManager.loadScriptForFn), + ); + } catch (err) { + await showErrorDialog({ + title: `Non-fatal: Error in ${args.type}/${args.id}`, + error: err, + }); } }); }); diff --git a/packages/client-api/src/user-config/get-script-manager.ts b/packages/client-api/src/user-config/get-script-manager.ts index 064eb233..2fefdfed 100644 --- a/packages/client-api/src/user-config/get-script-manager.ts +++ b/packages/client-api/src/user-config/get-script-manager.ts @@ -1,32 +1,47 @@ import { convertFileSrc } from '@tauri-apps/api/core'; import { join, homeDir } from '@tauri-apps/api/path'; -import { createStore } from 'solid-js/store'; import { createLogger } from '~/utils'; import type { ElementContext } from '../element-context.model'; const logger = createLogger('script-manager'); -const [modules, setModules] = createStore>>( - {}, -); +/** + * Map of asset paths to promises that resolve to the module. + */ +const modulesByPath: Record> = {}; + +/** + * Map of module paths to asset paths. + */ +const modulePathToAssetPath: Record = {}; /** * Abstraction over loading and invoking user-defined scripts. */ export function getScriptManager() { return { - loadScript, + loadScriptForFn, callFn, }; } -async function loadScript(path: string): Promise { - const scriptPath = await join(await homeDir(), '.glzr/zebar', path); - const scriptAssetPath = convertFileSrc(scriptPath); +async function loadScriptForFn(fnPath: string): Promise { + const { modulePath } = parseFnPath(fnPath); + + const assetPath = + modulePathToAssetPath[modulePath] ?? + (modulePathToAssetPath[modulePath] = convertFileSrc( + await join(await homeDir(), '.glzr/zebar', modulePath), + )); + + logger.info( + `Loading script at path '${assetPath}' for function path '${fnPath}'.`, + ); + + const importPromise = import(assetPath); + modulesByPath[assetPath] = importPromise; - const importPromise = import(scriptAssetPath); - setModules({ [path]: importPromise }); return importPromise; } @@ -35,20 +50,38 @@ async function callFn( event: Event, context: ElementContext, ): Promise { - const split = fnPath.split('#'); - const foundModule = modules[split[0]!]; + const { modulePath, functionName } = parseFnPath(fnPath); + const assetPath = modulePathToAssetPath[modulePath]; + const foundModule = modulesByPath[assetPath!]; if (!foundModule) { - throw new Error('Invalid function path'); + throw new Error(`No script found at function path '${fnPath}'.`); } - return foundModule.then(m => { - const fn = m[split[1]!]; + return foundModule.then(foundModule => { + const fn = foundModule[functionName!]; if (!fn) { - throw new Error('Invalid function path'); + throw new Error( + `No function with the name '${functionName}' at function path '${fnPath}'.`, + ); } return fn(event, context); }); } + +function parseFnPath(fnPath: string): { + modulePath: string; + functionName: string; +} { + const [modulePath, functionName] = fnPath.split('#'); + + // Should never been thrown, as the path is validated during config + // deserialization. + if (!modulePath || !functionName) { + throw new Error(`Invalid function path '${fnPath}'.`); + } + + return { modulePath, functionName }; +} diff --git a/packages/client-api/src/user-config/window/element-events-config.model.ts b/packages/client-api/src/user-config/window/element-events-config.model.ts index cc6c71c0..bcd6bef2 100644 --- a/packages/client-api/src/user-config/window/element-events-config.model.ts +++ b/packages/client-api/src/user-config/window/element-events-config.model.ts @@ -108,7 +108,12 @@ export const ElementEventsConfigSchema = z .array( z.object({ type: z.enum(HTML_EVENTS), - fn_path: z.string(), + fn_path: z + .string() + .regex( + /^(.+)#([a-zA-Z_$][a-zA-Z0-9_$]*)$/, + "Invalid function path. Needs to be in format 'path/to/my-script.js#functionName'.", + ), selector: z.string().optional(), }), ) From 9af74e752cf27d63627122b08728affb9a934004 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Tue, 2 Jul 2024 17:05:03 +0800 Subject: [PATCH 7/9] feat: add click event to glazewm template --- packages/desktop/resources/sample-config.yaml | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/desktop/resources/sample-config.yaml b/packages/desktop/resources/sample-config.yaml index 6fb7d027..a28bdae4 100644 --- a/packages/desktop/resources/sample-config.yaml +++ b/packages/desktop/resources/sample-config.yaml @@ -18,7 +18,7 @@ window/bar: # Width of the window in physical pixels. width: '{{ self.args.MONITOR_WIDTH }}' # Height of the window in physical pixels. - height: '45' + height: '40' # X-position of the window in physical pixels. position_x: '{{ self.args.MONITOR_X }}' # Y-position of the window in physical pixels. @@ -35,7 +35,9 @@ window/bar: # for a cheatsheet of available Nerdfonts icons. global_styles: | @import "https://www.nerdfonts.com/assets/css/webfont.css"; - # CSS/SCSS styles to apply to the root element within the window. + # CSS/SCSS styles to apply to the root element within the window. Using + # CSS nesting, we can also target nested elements (e.g. below we set the + # color and margin-right of icons). styles: | display: grid; grid-template-columns: 1fr 1fr 1fr; @@ -43,13 +45,16 @@ window/bar: height: 100%; color: #ffffffe6; font-family: ui-monospace, monospace; - font-size: 13px; + font-size: 12px; padding: 4px 24px; border-bottom: 1px solid #ffffff08; - background: linear-gradient( - rgba(14, 14, 28, 0.95), - rgba(26, 14, 28, 0.85), - ); + background: linear-gradient(rgba(0, 0, 0, 0.9), rgba(4, 1, 18, 0.85)); + + i { + color: #7481b2e1; + margin-right: 7px; + } + group/left: styles: | display: flex; @@ -76,6 +81,10 @@ window/bar: border-radius: 2px; } providers: ['glazewm'] + events: + - type: 'click' + fn_path: 'script.js#invokeMe' + selector: '.workspace' template: | @for (workspace of glazewm.workspacesOnMonitor) { @@ -100,11 +109,6 @@ window/bar: margin-left: 20px; } - i { - color: #7481b2e1; - margin-right: 7px; - } - template/network: providers: ['network'] template: | From 702baff28e32dcf78a104dcc9b5b67789fc22877 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Tue, 2 Jul 2024 18:38:07 +0800 Subject: [PATCH 8/9] feat: add support for event selectors --- packages/client-api/src/init-element.ts | 2 +- .../src/user-config/get-script-manager.ts | 69 ++++++++++--------- .../src/app/template-element.component.ts | 39 +++++++---- 3 files changed, 65 insertions(+), 45 deletions(-) diff --git a/packages/client-api/src/init-element.ts b/packages/client-api/src/init-element.ts index de1221ef..a84bfceb 100644 --- a/packages/client-api/src/init-element.ts +++ b/packages/client-api/src/init-element.ts @@ -92,7 +92,7 @@ export async function initElement( }); }); - // Import the scripts for the element. + // Preload the scripts used for the element's events. runWithOwner(args.owner, () => { createEffect(async () => { try { diff --git a/packages/client-api/src/user-config/get-script-manager.ts b/packages/client-api/src/user-config/get-script-manager.ts index 2fefdfed..8d054363 100644 --- a/packages/client-api/src/user-config/get-script-manager.ts +++ b/packages/client-api/src/user-config/get-script-manager.ts @@ -7,14 +7,14 @@ import type { ElementContext } from '../element-context.model'; const logger = createLogger('script-manager'); /** - * Map of asset paths to promises that resolve to the module. + * Map of module paths to asset paths. */ -const modulesByPath: Record> = {}; +const assetPathCache: Record = {}; /** - * Map of module paths to asset paths. + * Map of asset paths to promises that resolve to the module. */ -const modulePathToAssetPath: Record = {}; +const moduleCache: Record> = {}; /** * Abstraction over loading and invoking user-defined scripts. @@ -28,21 +28,7 @@ export function getScriptManager() { async function loadScriptForFn(fnPath: string): Promise { const { modulePath } = parseFnPath(fnPath); - - const assetPath = - modulePathToAssetPath[modulePath] ?? - (modulePathToAssetPath[modulePath] = convertFileSrc( - await join(await homeDir(), '.glzr/zebar', modulePath), - )); - - logger.info( - `Loading script at path '${assetPath}' for function path '${fnPath}'.`, - ); - - const importPromise = import(assetPath); - modulesByPath[assetPath] = importPromise; - - return importPromise; + return resolveModule(modulePath); } async function callFn( @@ -51,24 +37,43 @@ async function callFn( context: ElementContext, ): Promise { const { modulePath, functionName } = parseFnPath(fnPath); - const assetPath = modulePathToAssetPath[modulePath]; - const foundModule = modulesByPath[assetPath!]; + const foundModule = await resolveModule(modulePath); + const fn = foundModule[functionName]; + + if (!fn) { + throw new Error( + `No function with the name '${functionName}' at function path '${fnPath}'.`, + ); + } + + return fn(event, context); +} - if (!foundModule) { - throw new Error(`No script found at function path '${fnPath}'.`); +async function resolveModule(modulePath: string): Promise { + const assetPath = await getAssetPath(modulePath); + const foundModule = moduleCache[assetPath]; + + if (foundModule) { + return foundModule; } - return foundModule.then(foundModule => { - const fn = foundModule[functionName!]; + logger.info(`Loading script at path '${assetPath}'.`); + return (moduleCache[assetPath] = import(assetPath)); +} - if (!fn) { - throw new Error( - `No function with the name '${functionName}' at function path '${fnPath}'.`, - ); - } +/** + * Converts user-defined path to a URL that can be loaded by the webview. + */ +async function getAssetPath(modulePath: string): Promise { + const foundAssetPath = assetPathCache[modulePath]; + + if (foundAssetPath) { + return foundAssetPath; + } - return fn(event, context); - }); + return (assetPathCache[modulePath] = convertFileSrc( + await join(await homeDir(), '.glzr/zebar', modulePath), + )); } function parseFnPath(fnPath: string): { diff --git a/packages/client/src/app/template-element.component.ts b/packages/client/src/app/template-element.component.ts index ab5eeb9f..f192aeeb 100644 --- a/packages/client/src/app/template-element.component.ts +++ b/packages/client/src/app/template-element.component.ts @@ -10,6 +10,12 @@ export interface TemplateElementProps { context: ElementContext; } +interface ElementEventListener { + eventType: string; + eventCallback: (event: Event) => Promise; + selectorElement: Element; +} + export function TemplateElement(props: TemplateElementProps) { const config = props.context.parsedConfig; const logger = createLogger(`#${config.id}`); @@ -19,15 +25,13 @@ export function TemplateElement(props: TemplateElementProps) { const element = createRootElement(); // Currently active event listeners. - let listeners: { type: string; fn: (event: Event) => Promise }[] = - []; + let listeners: ElementEventListener[] = []; // Update the HTML element when the template changes. createEffect(() => { - clearEventListeners(); // @ts-ignore - TODO element.innerHTML = config.template; - addEventListeners(); + updateEventListeners(); }); onMount(() => logger.debug('Mounted')); @@ -40,21 +44,32 @@ export function TemplateElement(props: TemplateElementProps) { return element; } - function clearEventListeners() { - listeners.forEach(({ type, fn }) => - element.removeEventListener(type, fn), + function updateEventListeners() { + // Remove existing event listeners. + listeners.forEach(({ eventType, eventCallback, selectorElement }) => + selectorElement.removeEventListener(eventType, eventCallback), ); listeners = []; - } - function addEventListeners() { config.events.forEach(eventConfig => { - const callFn = (event: Event) => + const eventCallback = (event: Event) => scriptManager.callFn(eventConfig.fn_path, event, props.context); - element.addEventListener(eventConfig.type, callFn); - listeners.push({ type: eventConfig.type, fn: callFn }); + // Default to the root element if no selector is provided. + const selectorElement = eventConfig.selector + ? element.querySelector(eventConfig.selector) + : element; + + if (selectorElement) { + selectorElement.addEventListener(eventConfig.type, eventCallback); + + listeners.push({ + eventType: eventConfig.type, + eventCallback, + selectorElement, + }); + } }); } From 6e6c6acbc8bf745872f51d7816175406aab4349e Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Tue, 2 Jul 2024 18:48:38 +0800 Subject: [PATCH 9/9] refactor: remove todo --- packages/client-api/src/element-context.model.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/client-api/src/element-context.model.ts b/packages/client-api/src/element-context.model.ts index 87d0bfbd..82076893 100644 --- a/packages/client-api/src/element-context.model.ts +++ b/packages/client-api/src/element-context.model.ts @@ -49,8 +49,6 @@ interface BaseElementContext { */ providers: P; - // TODO: Consider adding `scripts` record where keys are script paths. - /** * Initializes a child group or template element. * @internal