diff --git a/packages/client-api/src/providers/create-provider.ts b/packages/client-api/src/providers/create-provider.ts index 5b6ec097..4ba1ce1c 100644 --- a/packages/client-api/src/providers/create-provider.ts +++ b/packages/client-api/src/providers/create-provider.ts @@ -2,6 +2,7 @@ import type { Owner } from 'solid-js'; import { createBatteryProvider } from './battery/create-battery-provider'; import { createCpuProvider } from './cpu/create-cpu-provider'; +import { createCustomProvider } from './custom/create-custom-provider'; import { createDateProvider } from './date/create-date-provider'; import { createGlazeWmProvider } from './glazewm/create-glazewm-provider'; import { createHostProvider } from './host/create-host-provider'; @@ -30,6 +31,8 @@ export async function createProvider( return createBatteryProvider(config, owner); case ProviderType.CPU: return createCpuProvider(config, owner); + case ProviderType.CUSTOM: + return createCustomProvider(config, elementContext, owner); case ProviderType.DATE: return createDateProvider(config, owner); case ProviderType.GLAZEWM: diff --git a/packages/client-api/src/providers/custom/create-custom-provider.ts b/packages/client-api/src/providers/custom/create-custom-provider.ts new file mode 100644 index 00000000..d23cf854 --- /dev/null +++ b/packages/client-api/src/providers/custom/create-custom-provider.ts @@ -0,0 +1,111 @@ +import { + createSignal, + onCleanup, + onMount, + runWithOwner, + type Owner, +} from 'solid-js'; + +import type { ElementContext } from '~/index'; +import { + getScriptManager, + type CustomProviderConfig, +} from '~/user-config'; +import { isEventTarget, type PickPartial } from '~/utils'; + +export interface CustomState { + state: unknown; +} + +export async function createCustomProvider( + config: CustomProviderConfig, + elementContext: PickPartial< + ElementContext, + 'parsedConfig' | 'providers' + >, + owner: Owner, +): Promise { + const [state, setState] = createSignal(); + const scriptManager = getScriptManager(); + + if (config.start_fn_path) + await scriptManager.callFn( + config.start_fn_path, + new Event('custom'), + elementContext, + ); + + if ('emitter_fn_path' in config) { + const eventTarget = await scriptManager.callFn( + config.emitter_fn_path, + new Event('custom'), + elementContext, + ); + + if (!isEventTarget(eventTarget)) + throw new TypeError(`Emitter function must return an event target.`); + + const listener = (event: Event) => { + if (event.type !== 'value') return; + + if (!('value' in event)) return; + + setState(event.value); + }; + + runWithOwner(owner, () => { + onMount(() => eventTarget.addEventListener('value', listener)); + onCleanup(async () => { + eventTarget.removeEventListener('value', listener); + + if (config.stop_fn_path) + await scriptManager.callFn( + config.stop_fn_path, + new Event('custom'), + elementContext, + ); + }); + }); + } else { + // run refresh fn first time to set initial state + setState( + await scriptManager.callFn( + config.refresh_fn_path, + new Event('custom'), + elementContext, + ), + ); + + // and then every refresh interval + const interval = setInterval( + async () => + setState( + await scriptManager.callFn( + config.refresh_fn_path, + new Event('custom'), + elementContext, + ), + ), + config.refresh_interval, + ); + + runWithOwner(owner, () => { + onCleanup(async () => { + clearInterval(interval); + + if (config.stop_fn_path) + await scriptManager.callFn( + config.stop_fn_path, + new Event('custom'), + elementContext, + ); + }); + }); + } + + return { + get state() { + return state(); + }, + }; +} diff --git a/packages/client-api/src/providers/index.ts b/packages/client-api/src/providers/index.ts index d0106b1c..1cda0cf1 100644 --- a/packages/client-api/src/providers/index.ts +++ b/packages/client-api/src/providers/index.ts @@ -1,5 +1,6 @@ export * from './battery/create-battery-provider'; export * from './cpu/create-cpu-provider'; +export * from './custom/create-custom-provider'; export * from './date/create-date-provider'; export * from './glazewm/create-glazewm-provider'; export * from './ip/create-ip-provider'; 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 33019e87..92df5364 100644 --- a/packages/client-api/src/user-config/get-script-manager.ts +++ b/packages/client-api/src/user-config/get-script-manager.ts @@ -1,7 +1,7 @@ import { convertFileSrc } from '@tauri-apps/api/core'; import { join, homeDir } from '@tauri-apps/api/path'; -import { createLogger } from '~/utils'; +import { createLogger, type PickPartial } from '~/utils'; import type { ElementContext } from '../element-context.model'; const logger = createLogger('script-manager'); @@ -34,7 +34,7 @@ async function loadScriptForFn(fnPath: string): Promise { async function callFn( fnPath: string, event: Event, - context: ElementContext, + context: PickPartial, ): Promise { const { modulePath, functionName } = parseFnPath(fnPath); const foundModule = await resolveModule(modulePath); diff --git a/packages/client-api/src/user-config/shared/fn-path.ts b/packages/client-api/src/user-config/shared/fn-path.ts new file mode 100644 index 00000000..71959c31 --- /dev/null +++ b/packages/client-api/src/user-config/shared/fn-path.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; + +export const FnPathSchema = z + .string() + .regex( + /^(.+)#([a-zA-Z_$][a-zA-Z0-9_$]*)$/, + "Invalid function path. Needs to be in format 'path/to/my-script.js#functionName'.", + ); diff --git a/packages/client-api/src/user-config/shared/index.ts b/packages/client-api/src/user-config/shared/index.ts index 7bb6ea76..e2c86901 100644 --- a/packages/client-api/src/user-config/shared/index.ts +++ b/packages/client-api/src/user-config/shared/index.ts @@ -1,3 +1,4 @@ export * from './boolean-like.model'; +export * from './fn-path'; export * from './get-child-configs'; export * from './with-dynamic-key'; 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 bcd6bef2..ea3d6772 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 @@ -1,4 +1,5 @@ import { z } from 'zod'; +import { FnPathSchema } from '../shared'; import type { Prettify } from '~/utils'; /** @@ -108,12 +109,7 @@ export const ElementEventsConfigSchema = z .array( z.object({ type: z.enum(HTML_EVENTS), - 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'.", - ), + fn_path: FnPathSchema, selector: z.string().optional(), }), ) diff --git a/packages/client-api/src/user-config/window/provider-config.model.ts b/packages/client-api/src/user-config/window/provider-config.model.ts index 7adfbaa0..7f8158e9 100644 --- a/packages/client-api/src/user-config/window/provider-config.model.ts +++ b/packages/client-api/src/user-config/window/provider-config.model.ts @@ -4,6 +4,7 @@ import type { Prettify } from '~/utils'; import { BatteryProviderConfigSchema, CpuProviderConfigSchema, + CustomProviderConfigSchema, DateProviderConfigSchema, GlazeWmProviderConfigSchema, HostProviderConfigSchema, @@ -20,6 +21,7 @@ import { export const ProviderConfigSchema = z.union([ BatteryProviderConfigSchema, CpuProviderConfigSchema, + CustomProviderConfigSchema, DateProviderConfigSchema, GlazeWmProviderConfigSchema, HostProviderConfigSchema, diff --git a/packages/client-api/src/user-config/window/provider-type.model.ts b/packages/client-api/src/user-config/window/provider-type.model.ts index 6ea9c221..62e63a34 100644 --- a/packages/client-api/src/user-config/window/provider-type.model.ts +++ b/packages/client-api/src/user-config/window/provider-type.model.ts @@ -3,6 +3,7 @@ import { z } from 'zod'; export enum ProviderType { BATTERY = 'battery', CPU = 'cpu', + CUSTOM = 'custom', DATE = 'date', GLAZEWM = 'glazewm', HOST = 'host', diff --git a/packages/client-api/src/user-config/window/providers/custom-provider.config.model.ts b/packages/client-api/src/user-config/window/providers/custom-provider.config.model.ts new file mode 100644 index 00000000..9549837c --- /dev/null +++ b/packages/client-api/src/user-config/window/providers/custom-provider.config.model.ts @@ -0,0 +1,24 @@ +import { z } from 'zod'; + +import { ProviderType } from '../provider-type.model'; +import { FnPathSchema } from '~/user-config/shared'; + +export const CustomProviderConfigSchema = z.union([ + z.object({ + type: z.literal(ProviderType.CUSTOM), + refresh_interval: z.coerce.number().default(5 * 1000), + start_fn_path: FnPathSchema.optional(), + refresh_fn_path: FnPathSchema, + stop_fn_path: FnPathSchema.optional(), + }), + z.object({ + type: z.literal(ProviderType.CUSTOM), + start_fn_path: FnPathSchema.optional(), + emitter_fn_path: FnPathSchema, + stop_fn_path: FnPathSchema.optional(), + }), +]); + +export type CustomProviderConfig = z.infer< + typeof CustomProviderConfigSchema +>; diff --git a/packages/client-api/src/user-config/window/providers/index.ts b/packages/client-api/src/user-config/window/providers/index.ts index caca0198..1fbddf49 100644 --- a/packages/client-api/src/user-config/window/providers/index.ts +++ b/packages/client-api/src/user-config/window/providers/index.ts @@ -1,5 +1,6 @@ export * from './battery-provider-config.model'; export * from './cpu-provider-config.model'; +export * from './custom-provider.config.model'; export * from './date-provider-config.model'; export * from './glazewm-provider-config.model'; export * from './host-provider-config.model'; diff --git a/packages/client-api/src/utils/index.ts b/packages/client-api/src/utils/index.ts index 85438cd5..ea5885da 100644 --- a/packages/client-api/src/utils/index.ts +++ b/packages/client-api/src/utils/index.ts @@ -5,5 +5,6 @@ export * from './create-getter-proxy'; export * from './create-logger'; export * from './create-string-scanner'; export * from './get-coordinate-distance'; +export * from './is-event-target'; export * from './simple-hash'; export * from './to-css-selector'; diff --git a/packages/client-api/src/utils/is-event-target.ts b/packages/client-api/src/utils/is-event-target.ts new file mode 100644 index 00000000..3d19e241 --- /dev/null +++ b/packages/client-api/src/utils/is-event-target.ts @@ -0,0 +1,3 @@ +export function isEventTarget(obj: any): obj is EventTarget { + return obj && typeof obj.addEventListener === 'function'; +}