From 6312a17b86588930d6c61a013f002aa42c86ff3b Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 22 Aug 2024 01:39:40 +0800 Subject: [PATCH 001/138] feat: delete templating-related files; add `ZebarContext` interface --- .../client-api/src/desktop/current-window.ts | 6 +- .../src/desktop/shared/window-info.model.ts | 7 - .../client-api/src/element-context.model.ts | 79 ----- packages/client-api/src/element-type.model.ts | 5 - packages/client-api/src/index.ts | 22 +- packages/client-api/src/init-element.ts | 150 ---------- packages/client-api/src/init-window.ts | 141 --------- packages/client-api/src/init.ts | 55 ++++ packages/client-api/src/shims.d.ts | 5 - .../template-engine/get-template-engine.ts | 38 --- .../client-api/src/template-engine/index.ts | 5 - .../src/template-engine/rendering/index.ts | 1 - .../rendering/render-template-nodes.ts | 167 ----------- .../src/template-engine/shared/index.ts | 1 - .../template-engine/shared/template-error.ts | 8 - .../token-parsing/for-statement-node.ts | 8 - .../token-parsing/if-statement-node.model.ts | 19 -- .../template-engine/token-parsing/index.ts | 8 - .../token-parsing/interpolation-node.model.ts | 6 - .../token-parsing/parse-tokens.ts | 211 ------------- .../switch-statement-node.model.ts | 19 -- .../token-parsing/template-node-type.model.ts | 7 - .../token-parsing/template-node.model.ts | 12 - .../token-parsing/text-node.model.ts | 6 - .../src/template-engine/tokenizing/index.ts | 3 - .../tokenizing/token-type.model.ts | 40 --- .../template-engine/tokenizing/token.model.ts | 8 - .../tokenizing/tokenize-template.ts | 277 ------------------ .../user-config/get-parsed-element-config.ts | 88 ------ .../src/user-config/get-script-manager.ts | 92 ------ .../src/user-config/get-style-builder.ts | 47 --- .../src/user-config/get-user-config.ts | 35 --- .../src/user-config/global-config.model.ts | 11 - packages/client-api/src/user-config/index.ts | 10 +- .../src/user-config/parse-with-schema.ts | 26 -- .../user-config/shared/boolean-like.model.ts | 5 - .../user-config/shared/get-child-configs.ts | 34 --- .../src/user-config/shared/index.ts | 3 - .../user-config/shared/with-dynamic-key.ts | 68 ----- .../src/user-config/user-config.model.ts | 21 -- .../src/user-config/window-config.ts | 66 +++++ .../window/base-element-config.model.ts | 18 -- .../window/element-events-config.model.ts | 124 -------- .../user-config/window/group-config.model.ts | 21 -- .../src/user-config/window/index.ts | 7 - .../window/providers-config.model.ts | 20 -- .../window/template-config.model.ts | 10 - .../user-config/window/window-config.model.ts | 35 --- .../src/user-config/window/z-order.model.ts | 7 - packages/client-api/src/utils/clsx.ts | 38 --- .../src/utils/create-getter-proxy.ts | 45 --- .../src/utils/create-string-scanner.ts | 65 ---- packages/client-api/src/utils/index.ts | 5 - .../client-api/src/utils/to-css-selector.ts | 12 - .../src/utils/types/pick-partial.ts | 2 - .../client-api/src/zebar-context.model.ts | 56 ++++ packages/desktop/resources/draft.yaml | 48 +++ packages/desktop/resources/index.html | 20 ++ 58 files changed, 251 insertions(+), 2102 deletions(-) delete mode 100644 packages/client-api/src/desktop/shared/window-info.model.ts delete mode 100644 packages/client-api/src/element-context.model.ts delete mode 100644 packages/client-api/src/element-type.model.ts delete mode 100644 packages/client-api/src/init-element.ts delete mode 100644 packages/client-api/src/init-window.ts create mode 100644 packages/client-api/src/init.ts delete mode 100644 packages/client-api/src/template-engine/get-template-engine.ts delete mode 100644 packages/client-api/src/template-engine/index.ts delete mode 100644 packages/client-api/src/template-engine/rendering/index.ts delete mode 100644 packages/client-api/src/template-engine/rendering/render-template-nodes.ts delete mode 100644 packages/client-api/src/template-engine/shared/index.ts delete mode 100644 packages/client-api/src/template-engine/shared/template-error.ts delete mode 100644 packages/client-api/src/template-engine/token-parsing/for-statement-node.ts delete mode 100644 packages/client-api/src/template-engine/token-parsing/if-statement-node.model.ts delete mode 100644 packages/client-api/src/template-engine/token-parsing/index.ts delete mode 100644 packages/client-api/src/template-engine/token-parsing/interpolation-node.model.ts delete mode 100644 packages/client-api/src/template-engine/token-parsing/parse-tokens.ts delete mode 100644 packages/client-api/src/template-engine/token-parsing/switch-statement-node.model.ts delete mode 100644 packages/client-api/src/template-engine/token-parsing/template-node-type.model.ts delete mode 100644 packages/client-api/src/template-engine/token-parsing/template-node.model.ts delete mode 100644 packages/client-api/src/template-engine/token-parsing/text-node.model.ts delete mode 100644 packages/client-api/src/template-engine/tokenizing/index.ts delete mode 100644 packages/client-api/src/template-engine/tokenizing/token-type.model.ts delete mode 100644 packages/client-api/src/template-engine/tokenizing/token.model.ts delete mode 100644 packages/client-api/src/template-engine/tokenizing/tokenize-template.ts delete mode 100644 packages/client-api/src/user-config/get-parsed-element-config.ts delete mode 100644 packages/client-api/src/user-config/get-script-manager.ts delete mode 100644 packages/client-api/src/user-config/get-style-builder.ts delete mode 100644 packages/client-api/src/user-config/get-user-config.ts delete mode 100644 packages/client-api/src/user-config/global-config.model.ts delete mode 100644 packages/client-api/src/user-config/parse-with-schema.ts delete mode 100644 packages/client-api/src/user-config/shared/boolean-like.model.ts delete mode 100644 packages/client-api/src/user-config/shared/get-child-configs.ts delete mode 100644 packages/client-api/src/user-config/shared/index.ts delete mode 100644 packages/client-api/src/user-config/shared/with-dynamic-key.ts delete mode 100644 packages/client-api/src/user-config/user-config.model.ts create mode 100644 packages/client-api/src/user-config/window-config.ts delete mode 100644 packages/client-api/src/user-config/window/base-element-config.model.ts delete mode 100644 packages/client-api/src/user-config/window/element-events-config.model.ts delete mode 100644 packages/client-api/src/user-config/window/group-config.model.ts delete mode 100644 packages/client-api/src/user-config/window/providers-config.model.ts delete mode 100644 packages/client-api/src/user-config/window/template-config.model.ts delete mode 100644 packages/client-api/src/user-config/window/window-config.model.ts delete mode 100644 packages/client-api/src/user-config/window/z-order.model.ts delete mode 100644 packages/client-api/src/utils/clsx.ts delete mode 100644 packages/client-api/src/utils/create-getter-proxy.ts delete mode 100644 packages/client-api/src/utils/create-string-scanner.ts delete mode 100644 packages/client-api/src/utils/to-css-selector.ts delete mode 100644 packages/client-api/src/utils/types/pick-partial.ts create mode 100644 packages/client-api/src/zebar-context.model.ts create mode 100644 packages/desktop/resources/draft.yaml create mode 100644 packages/desktop/resources/index.html diff --git a/packages/client-api/src/desktop/current-window.ts b/packages/client-api/src/desktop/current-window.ts index c2fe39a2..a1b93b07 100644 --- a/packages/client-api/src/desktop/current-window.ts +++ b/packages/client-api/src/desktop/current-window.ts @@ -5,7 +5,7 @@ import { type Window, } from '@tauri-apps/api/window'; -import type { ZOrder } from '~/user-config'; +import { WindowZOrder } from '~/user-config'; import { createLogger } from '~/utils'; import { setAlwaysOnTop, setSkipTaskbar } from './desktop-commands'; @@ -17,7 +17,7 @@ export interface WindowPosition { } export interface WindowStyles { - zOrder: ZOrder; + zOrder: WindowZOrder; shownInTaskbar: boolean; resizable: boolean; } @@ -66,7 +66,7 @@ export async function setWindowStyles(styles: Partial) { ]); } -async function setWindowZOrder(window: Window, zOrder?: ZOrder) { +async function setWindowZOrder(window: Window, zOrder?: WindowZOrder) { if (zOrder === 'always_on_bottom') { await window.setAlwaysOnBottom(true); } else if (zOrder === 'always_on_top') { diff --git a/packages/client-api/src/desktop/shared/window-info.model.ts b/packages/client-api/src/desktop/shared/window-info.model.ts deleted file mode 100644 index 6d1e0e11..00000000 --- a/packages/client-api/src/desktop/shared/window-info.model.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface WindowInfo { - x: number; - y: number; - width: number; - height: number; - scaleFactor: number; -} diff --git a/packages/client-api/src/element-context.model.ts b/packages/client-api/src/element-context.model.ts deleted file mode 100644 index ba4f79bc..00000000 --- a/packages/client-api/src/element-context.model.ts +++ /dev/null @@ -1,79 +0,0 @@ -import type { - GlobalConfig, - GroupConfig, - TemplateConfig, - WindowConfig, -} from '~/user-config'; -import { ElementType } from './element-type.model'; - -export type ElementConfig = WindowConfig | GroupConfig | TemplateConfig; - -interface BaseElementContext { - /** - * ID of this element. - */ - id: string; - - /** - * Type of this element. - */ - type: ElementType; - - /** - * Unparsed config for this element. - */ - rawConfig: unknown; - - /** - * Parsed config for this element. - */ - parsedConfig: C; - - /** - * Global user config. - */ - globalConfig: GlobalConfig; - - /** - * Args used to open the window. - */ - args: Record; - - /** - * Environment variables when window was opened. - */ - env: Record; - - /** - * Map of this element's providers and their variables. - */ - providers: P; - - /** - * Opens a new window by its ID. - */ - openWindow: ( - windowId: string, - args?: Record, - ) => Promise; - - /** - * Initializes a child group or template element. - * @internal - */ - initChildElement: ( - id: string, - ) => Promise; -} - -export type WindowContext

= BaseElementContext; -export type GroupContext

= BaseElementContext; -export type TemplateContext

= BaseElementContext< - TemplateConfig, - P ->; - -export type ElementContext = - | WindowContext - | GroupContext - | TemplateContext; diff --git a/packages/client-api/src/element-type.model.ts b/packages/client-api/src/element-type.model.ts deleted file mode 100644 index 781bd35c..00000000 --- a/packages/client-api/src/element-type.model.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum ElementType { - WINDOW = 'window', - GROUP = 'group', - TEMPLATE = 'template', -} diff --git a/packages/client-api/src/index.ts b/packages/client-api/src/index.ts index b111d0ca..429d5d7c 100644 --- a/packages/client-api/src/index.ts +++ b/packages/client-api/src/index.ts @@ -1,20 +1,2 @@ -export { createLogger, toCssSelector } from './utils'; -export { - getScriptManager, - getChildConfigs, - type GlobalConfig, - type WindowConfig, - type GroupConfig, - type TemplateConfig, - type ProviderConfig, - type ProvidersConfig, -} from './user-config'; -export { - type ElementContext, - type ElementConfig, - type WindowContext, - type GroupContext, - type TemplateContext, -} from './element-context.model'; -export { ElementType } from './element-type.model'; -export { initWindow, initWindowAsync } from './init-window'; +export { type WindowConfig } from './user-config'; +export { init, initAsync } from './init'; diff --git a/packages/client-api/src/init-element.ts b/packages/client-api/src/init-element.ts deleted file mode 100644 index f2fce8bb..00000000 --- a/packages/client-api/src/init-element.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { - type Accessor, - type Owner, - createEffect, - runWithOwner, -} from 'solid-js'; -import { createStore } from 'solid-js/store'; - -import { - getStyleBuilder, - getParsedElementConfig, - getChildConfigs, - type GlobalConfig, - getScriptManager, -} from './user-config'; -import { getElementProviders } from './providers'; -import type { ElementContext } from './element-context.model'; -import { ElementType } from './element-type.model'; -import { createLogger, type PickPartial } from './utils'; -import { openWindow, showErrorDialog } from './desktop'; - -const logger = createLogger('init-element'); - -export interface InitElementArgs { - id: string; - type: ElementType; - rawConfig: unknown; - globalConfig: GlobalConfig; - args: Record; - env: Record; - ancestorProviders: Accessor>[]; - owner: Owner; -} - -export async function initElement( - args: InitElementArgs, -): Promise { - try { - const styleBuilder = getStyleBuilder(); - const scriptManager = getScriptManager(); - const childConfigs = getChildConfigs(args.rawConfig); - - // Create partial element context; `providers` and `parsedConfig` are set later. - const [elementContext, setElementContext] = createStore< - PickPartial - >({ - id: args.id, - type: args.type, - rawConfig: args.rawConfig, - globalConfig: args.globalConfig, - args: args.args, - env: args.env, - openWindow, - initChildElement, - providers: undefined, - parsedConfig: undefined, - }); - - const { element, merged } = await getElementProviders( - elementContext, - args.ancestorProviders, - args.owner, - ); - - setElementContext('providers', merged); - - const parsedConfig = getParsedElementConfig( - elementContext as PickPartial, - args.owner, - ); - - // Since `parsedConfig` and `providers` are set after initializing providers - // and parsing the element config, they are initially unavailable on 'self' - // provider. - setElementContext('parsedConfig', parsedConfig); - - // Build the CSS for the element. - runWithOwner(args.owner, () => { - createEffect(async () => { - if (parsedConfig.styles) { - try { - styleBuilder.buildElementStyles( - parsedConfig.id, - parsedConfig.styles, - ); - } catch (err) { - await showErrorDialog({ - title: `Non-fatal: Error in ${args.type}/${args.id}`, - error: err, - }); - } - } - }); - }); - - // Preload the scripts used for the element's events. - runWithOwner(args.owner, () => { - 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, - }); - } - }); - }); - - async function initChildElement(id: string) { - const childConfig = childConfigs.find( - childConfig => childConfig.id === id, - ); - - // Check whether an element with the given ID exists in the config. - if (!childConfig) { - return null; - } - - return initElement({ - id, - type: childConfig.type, - rawConfig: childConfig.config, - globalConfig: args.globalConfig, - args: args.args, - env: args.env, - ancestorProviders: [...(args.ancestorProviders ?? []), element], - owner: args.owner, - }); - } - - return elementContext as ElementContext; - } catch (err) { - // Let error immediately bubble up if element is a window. - if (args.type !== ElementType.WINDOW) { - logger.error('Failed to initialize element:', err); - - await showErrorDialog({ - title: `Non-fatal: Error in ${args.type}/${args.id}`, - error: err, - }); - } - - throw err; - } -} diff --git a/packages/client-api/src/init-window.ts b/packages/client-api/src/init-window.ts deleted file mode 100644 index 1aa33ddc..00000000 --- a/packages/client-api/src/init-window.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { getCurrentWindow } from '@tauri-apps/api/window'; -import { createEffect, getOwner, runWithOwner } from 'solid-js'; - -import { - GlobalConfigSchema, - type UserConfig, - getUserConfig, - getStyleBuilder, - parseWithSchema, -} from './user-config'; -import { - getOpenWindowArgs, - setWindowPosition, - setWindowStyles, - showErrorDialog, - type WindowPosition, - type WindowStyles, -} from './desktop'; -import { initElement } from './init-element'; -import type { WindowContext } from './element-context.model'; -import { ElementType } from './element-type.model'; -import { createLogger } from '~/utils'; - -const logger = createLogger('init-window'); - -export function initWindow(callback: (context: WindowContext) => void) { - initWindowAsync().then(callback); -} - -/** - * Handles initialization. - * - * Steps involved: - * * Reading the user config - * * Creation of the window context - * * Positioning the window - * * Building CSS and appending it to `` - */ -export async function initWindowAsync(): Promise { - // Window ID is moved out of the try-catch to improve error messages. - let windowId: string | null = null; - - try { - // TODO: Create new root if owner is null. - const owner = getOwner()!; - const config = await getUserConfig(); - const styleBuilder = getStyleBuilder(); - - const openArgs = - window.__ZEBAR_OPEN_ARGS ?? - (await getOpenWindowArgs(getCurrentWindow().label)); - - windowId = openArgs.windowId; - const windowConfig = (config as UserConfig)[ - `window/${windowId}` as const - ]; - - if (!windowConfig) { - throw new Error( - `Window \`${windowId}\` isn\'t defined in the config. ` + - `Is there a property for \`window/${windowId}\`?`, - ); - } - - const globalConfig = parseWithSchema( - GlobalConfigSchema.strip(), - (config as UserConfig)?.global ?? {}, - ); - - const windowContext = (await initElement({ - id: windowId, - type: ElementType.WINDOW, - rawConfig: windowConfig, - globalConfig, - args: openArgs.args, - env: openArgs.env, - ancestorProviders: [], - owner, - })) as WindowContext; - - // Set global CSS styles. - runWithOwner(owner, () => { - createEffect(async () => { - if (windowContext.parsedConfig.global_styles) { - try { - styleBuilder.buildGlobalStyles( - windowContext.parsedConfig.global_styles, - ); - } catch (err) { - await showErrorDialog({ - title: `Non-fatal: Error in window/${windowId}`, - error: err, - }); - } - } - }); - }); - - // Set window position and apply window styles/effects. - runWithOwner(owner, () => { - createEffect(async () => { - // Create `styles` and `position` variables prior to awaiting, such that - // dependencies are tracked successfully within the effect. - const styles: Partial = { - zOrder: windowContext.parsedConfig.z_order, - shownInTaskbar: - windowContext.parsedConfig.show_in_taskbar || - windowContext.parsedConfig.shown_in_taskbar, - resizable: windowContext.parsedConfig.resizable, - }; - - const position: Partial = { - x: windowContext.parsedConfig.position_x, - y: windowContext.parsedConfig.position_y, - width: windowContext.parsedConfig.width, - height: windowContext.parsedConfig.height, - }; - - await setWindowStyles(styles); - await setWindowPosition(position); - }); - }); - - return windowContext; - } catch (err) { - logger.error('Failed to initialize window:', err); - - await showErrorDialog({ - title: windowId - ? `Fatal: Error in window/${windowId}` - : 'Fatal: Error in unknown window', - error: err, - }); - - // Error during window initialization is unrecoverable, so we close - // the window. - await getCurrentWindow().close(); - - throw err; - } -} diff --git a/packages/client-api/src/init.ts b/packages/client-api/src/init.ts new file mode 100644 index 00000000..442b7b85 --- /dev/null +++ b/packages/client-api/src/init.ts @@ -0,0 +1,55 @@ +import { getCurrentWindow } from '@tauri-apps/api/window'; +import { getOwner } from 'solid-js'; + +import { getOpenWindowArgs, openWindow, showErrorDialog } from './desktop'; +import { createLogger } from '~/utils'; +import type { ZebarContext } from './zebar-context.model'; + +const logger = createLogger('init-window'); + +/** + * Handles initialization. + */ +export function init(callback: (context: ZebarContext) => void) { + initAsync().then(callback); +} + +/** + * Handles initialization. + */ +export async function initAsync(): Promise { + try { + // TODO: Create new root if owner is null. + const owner = getOwner()!; + + const windowState = + window.__ZEBAR_OPEN_ARGS ?? + (await getOpenWindowArgs(getCurrentWindow().label)); + + return { + config: windowState.config, + providers: windowState.providers, + openWindow, + // @ts-ignore - TODO + currentWindow: {}, + // @ts-ignore - TODO + allWindows: [], + // @ts-ignore - TODO + currentMonitor: {}, + // @ts-ignore - TODO + allMonitors: [], + }; + } catch (err) { + logger.error('Failed to initialize window:', err); + + await showErrorDialog({ + title: 'Failed to initialize window', + error: err, + }); + + // Error during window initialization is unrecoverable, so we close + // the window. + getCurrentWindow().close(); + throw err; + } +} diff --git a/packages/client-api/src/shims.d.ts b/packages/client-api/src/shims.d.ts index 57f930c1..20848cf9 100644 --- a/packages/client-api/src/shims.d.ts +++ b/packages/client-api/src/shims.d.ts @@ -2,8 +2,3 @@ interface Window { __TAURI__: any; __ZEBAR_OPEN_ARGS: import('./desktop').OpenWindowArgs; } - -declare module '*.html' { - const src: string; - export default src; -} diff --git a/packages/client-api/src/template-engine/get-template-engine.ts b/packages/client-api/src/template-engine/get-template-engine.ts deleted file mode 100644 index 07cd51a1..00000000 --- a/packages/client-api/src/template-engine/get-template-engine.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { createStore, reconcile } from 'solid-js/store'; - -import { type TemplateNode, parseTokens } from './token-parsing'; -import { renderTemplateNodes } from './rendering'; -import { tokenizeTemplate } from './tokenizing'; - -const [cache, setCache] = createStore>({}); - -export interface TemplateEngine { - render: (template: string, context: Record) => string; - clearCache: () => void; -} - -export function getTemplateEngine(): TemplateEngine { - return { - render, - clearCache, - }; -} - -function render(template: string, context: Record) { - const cacheHit = cache[template]; - - if (cacheHit) { - return renderTemplateNodes(cacheHit, context); - } - - // Tokenize and parse the template. Cache the result. - const tokens = tokenizeTemplate(template); - const parsed = parseTokens(tokens); - setCache(template, parsed); - - return renderTemplateNodes(parsed, context); -} - -function clearCache() { - setCache(reconcile({})); -} diff --git a/packages/client-api/src/template-engine/index.ts b/packages/client-api/src/template-engine/index.ts deleted file mode 100644 index fa438aba..00000000 --- a/packages/client-api/src/template-engine/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './rendering'; -export * from './shared'; -export * from './token-parsing'; -export * from './tokenizing'; -export * from './get-template-engine'; diff --git a/packages/client-api/src/template-engine/rendering/index.ts b/packages/client-api/src/template-engine/rendering/index.ts deleted file mode 100644 index abd9a129..00000000 --- a/packages/client-api/src/template-engine/rendering/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './render-template-nodes'; diff --git a/packages/client-api/src/template-engine/rendering/render-template-nodes.ts b/packages/client-api/src/template-engine/rendering/render-template-nodes.ts deleted file mode 100644 index c96320d1..00000000 --- a/packages/client-api/src/template-engine/rendering/render-template-nodes.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { - type ForStatementNode, - type IfStatementNode, - type InterpolationNode, - type SwitchStatementNode, - type TemplateNode, - TemplateNodeType, - type TextNode, -} from '../token-parsing'; -import { TemplateError } from '../shared'; - -export interface RenderTransforms { - transformExpression?: (expression: unknown) => unknown; -} - -export interface RenderContext { - global: Record; - local: Record[]; -} - -/** Pattern for the expression in a for loop statement. */ -const FOR_LOOP_EXPRESSION_PATTERN = - /^\s*([(),\s0-9A-Za-z_$]*)\s+of\s+(.*)/; - -/** Pattern for the loop variable on the left-side of a for loop expression. */ -const FOR_LOOP_VARIABLE_PATTERN = - /^\(?\s*([0-9A-Za-z_$]*)\s*,?\s*([0-9A-Za-z_$]*)/; - -/** - * Takes an abstract syntax tree and renders it to a string. - */ -export function renderTemplateNodes( - nodes: TemplateNode[], - globalContext: Record, -) { - const context: RenderContext = { - global: globalContext, - local: [], - }; - - function visitAll(nodes: TemplateNode[]): string { - return nodes.map(node => visitOne(node)).join(''); - } - - function visitOne(node: TemplateNode): string { - switch (node.type) { - case TemplateNodeType.TEXT: - return visitTextNode(node); - case TemplateNodeType.INTERPOLATION: - return visitInterpolationNode(node); - case TemplateNodeType.IF_STATEMENT: - return visitIfStatementNode(node); - case TemplateNodeType.FOR_STATEMENT: - return visitForStatementNode(node); - case TemplateNodeType.SWITCH_STATEMENT: - return visitSwitchStatementNode(node); - } - } - - function visitTextNode(node: TextNode): string { - return node.text; - } - - function visitInterpolationNode(node: InterpolationNode): string { - return evalExpression(node.expression); - } - - function visitIfStatementNode(node: IfStatementNode): string { - for (const branch of node.branches) { - const shouldVisit = - branch.type === 'else' || - Boolean(evalExpression(branch.expression)); - - if (shouldVisit) { - return visitAll(branch.children); - } - } - - return ''; - } - - function visitForStatementNode(node: ForStatementNode): string { - const { loopVariable, indexVariable, iterable } = parseForExpression( - node.expression, - ); - - return iterable - .map((el, index) => { - // Push loop variable and index (optionally) to local context. - context.local.push({ - [loopVariable]: el, - ...(indexVariable ? { [indexVariable]: index } : {}), - }); - - const result = visitAll(node.children); - context.local.pop(); - - return result; - }) - .join(''); - } - - function parseForExpression(expression: string) { - try { - const expressionMatch = expression.match( - FOR_LOOP_EXPRESSION_PATTERN, - ); - const [_, loopVariableExpression, iterable] = expressionMatch ?? []; - - if (!loopVariableExpression || !iterable) { - throw new Error(); - } - - const loopVariableMatch = loopVariableExpression.match( - FOR_LOOP_VARIABLE_PATTERN, - ); - const [__, loopVariable, indexVariable] = loopVariableMatch ?? []; - - if (!loopVariable) { - throw new Error(); - } - - return { - loopVariable, - indexVariable, - iterable: evalExpression(iterable) as unknown[], - }; - } catch (err) { - throw new TemplateError( - "@for loop doesn't have a valid expression. Must be in the format '@for (item of items) { ... }'.", - 0, - ); - } - } - - function visitSwitchStatementNode(node: SwitchStatementNode): string { - const value = evalExpression(node.expression); - - for (const branch of node.branches) { - const shouldVisit = - branch.type === 'default' || - value === evalExpression(branch.expression); - - if (shouldVisit) { - return visitAll(branch.children); - } - } - - return ''; - } - - function evalExpression(expression: string) { - const evalFn = new Function( - 'global', - 'local', - `with (global) { with (local) { return ${expression} } }`, - ); - - return evalFn( - context.global, - context.local.reduce((acc, e) => ({ ...acc, ...e }), {}), - ); - } - - // Render and trim any leading/trailing whitespace. - return visitAll(nodes).trim(); -} diff --git a/packages/client-api/src/template-engine/shared/index.ts b/packages/client-api/src/template-engine/shared/index.ts deleted file mode 100644 index 8c094aeb..00000000 --- a/packages/client-api/src/template-engine/shared/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './template-error'; diff --git a/packages/client-api/src/template-engine/shared/template-error.ts b/packages/client-api/src/template-engine/shared/template-error.ts deleted file mode 100644 index af9be6c4..00000000 --- a/packages/client-api/src/template-engine/shared/template-error.ts +++ /dev/null @@ -1,8 +0,0 @@ -export class TemplateError extends Error { - public templateIndex: number; - - constructor(message: string, templateIndex: number) { - super(message); - this.templateIndex = templateIndex; - } -} diff --git a/packages/client-api/src/template-engine/token-parsing/for-statement-node.ts b/packages/client-api/src/template-engine/token-parsing/for-statement-node.ts deleted file mode 100644 index 323fc647..00000000 --- a/packages/client-api/src/template-engine/token-parsing/for-statement-node.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { TemplateNode } from './template-node.model'; -import { TemplateNodeType } from './template-node-type.model'; - -export interface ForStatementNode { - type: TemplateNodeType.FOR_STATEMENT; - expression: string; - children: TemplateNode[]; -} diff --git a/packages/client-api/src/template-engine/token-parsing/if-statement-node.model.ts b/packages/client-api/src/template-engine/token-parsing/if-statement-node.model.ts deleted file mode 100644 index d97c822e..00000000 --- a/packages/client-api/src/template-engine/token-parsing/if-statement-node.model.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { TemplateNodeType } from './template-node-type.model'; -import type { TemplateNode } from './template-node.model'; - -export interface IfBranch { - type: 'if' | 'else if'; - expression: string; - children: TemplateNode[]; -} - -export interface ElseBranch { - type: 'else'; - expression: null; - children: TemplateNode[]; -} - -export interface IfStatementNode { - type: TemplateNodeType.IF_STATEMENT; - branches: (IfBranch | ElseBranch)[]; -} diff --git a/packages/client-api/src/template-engine/token-parsing/index.ts b/packages/client-api/src/template-engine/token-parsing/index.ts deleted file mode 100644 index 2ef36488..00000000 --- a/packages/client-api/src/template-engine/token-parsing/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * from './for-statement-node'; -export * from './if-statement-node.model'; -export * from './interpolation-node.model'; -export * from './parse-tokens'; -export * from './switch-statement-node.model'; -export * from './template-node-type.model'; -export * from './template-node.model'; -export * from './text-node.model'; diff --git a/packages/client-api/src/template-engine/token-parsing/interpolation-node.model.ts b/packages/client-api/src/template-engine/token-parsing/interpolation-node.model.ts deleted file mode 100644 index 5dd191c1..00000000 --- a/packages/client-api/src/template-engine/token-parsing/interpolation-node.model.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { TemplateNodeType } from './template-node-type.model'; - -export interface InterpolationNode { - type: TemplateNodeType.INTERPOLATION; - expression: string; -} diff --git a/packages/client-api/src/template-engine/token-parsing/parse-tokens.ts b/packages/client-api/src/template-engine/token-parsing/parse-tokens.ts deleted file mode 100644 index e067d8c1..00000000 --- a/packages/client-api/src/template-engine/token-parsing/parse-tokens.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { TemplateError } from '../shared'; -import { type Token, TokenType } from '../tokenizing'; -import type { ForStatementNode } from './for-statement-node'; -import type { - IfStatementNode, - IfBranch, - ElseBranch, -} from './if-statement-node.model'; -import type { InterpolationNode } from './interpolation-node.model'; -import type { - SwitchStatementNode, - CaseBranch, - DefaultBranch, -} from './switch-statement-node.model'; -import { TemplateNodeType } from './template-node-type.model'; -import type { TemplateNode } from './template-node.model'; -import type { TextNode } from './text-node.model'; - -export function parseTokens(tokens: Token[]) { - let cursor = 0; - const nodes: TemplateNode[] = []; - - while (cursor < tokens.length) { - const node = parseStandaloneToken(tokens[cursor]!); - nodes.push(node); - cursor += 1; - } - - function parseStandaloneToken(token: Token): TemplateNode { - switch (token.type) { - case TokenType.TEXT: - return parseText(token); - case TokenType.OPEN_INTERPOLATION: - return parseInterpolation(token); - case TokenType.IF_STATEMENT: - return parseIfStatement(token); - case TokenType.FOR_STATEMENT: - return parseForStatement(token); - case TokenType.SWITCH_STATEMENT: - return parseSwitchStatement(token); - case TokenType.SWITCH_CASE_STATEMENT: - throw new TemplateError( - 'Cannot use @case without a @switch statement.', - token.startIndex, - ); - case TokenType.SWITCH_DEFAULT_STATEMENT: - throw new TemplateError( - 'Cannot use @default without a @switch statement.', - token.startIndex, - ); - case TokenType.ELSE_IF_STATEMENT: - throw new TemplateError( - 'Cannot use @elseif without an @if statement.', - token.startIndex, - ); - case TokenType.ELSE_STATEMENT: - throw new TemplateError( - 'Cannot use @else without an @if statement.', - token.startIndex, - ); - default: - throw new TemplateError( - `Unknown token type '${token.type}'.`, - token.startIndex, - ); - } - } - - function parseNestedTokens(): TemplateNode[] { - const nodes: TemplateNode[] = []; - let next = tokens[cursor + 1]; - - while ( - // TODO: Add null check here for `next`. - next.type === TokenType.TEXT || - next.type === TokenType.OPEN_INTERPOLATION || - next.type === TokenType.IF_STATEMENT || - next.type === TokenType.FOR_STATEMENT || - next.type === TokenType.SWITCH_STATEMENT - ) { - cursor += 1; - const node = parseStandaloneToken(next); - nodes.push(node); - next = tokens[cursor + 1]; - } - - return nodes; - } - - function parseText(token: Token): TextNode { - return { - type: TemplateNodeType.TEXT, - text: token.substring, - }; - } - - function parseInterpolation(_token: Token): InterpolationNode { - const expression = need(TokenType.EXPRESSION).substring; - need(TokenType.CLOSE_INTERPOLATION); - - return { - type: TemplateNodeType.INTERPOLATION, - expression, - }; - } - - function parseIfStatement(_token: Token): IfStatementNode { - const branches: (IfBranch | ElseBranch)[] = []; - - const expression = need(TokenType.EXPRESSION).substring; - need(TokenType.OPEN_BLOCK); - const children = parseNestedTokens(); - - branches.push({ type: 'if', expression, children }); - need(TokenType.CLOSE_BLOCK); - - while (expect(TokenType.ELSE_IF_STATEMENT)) { - const expression = need(TokenType.EXPRESSION).substring; - need(TokenType.OPEN_BLOCK).substring; - const children = parseNestedTokens(); - - branches.push({ type: 'else if', expression, children }); - need(TokenType.CLOSE_BLOCK); - } - - if (expect(TokenType.ELSE_STATEMENT)) { - need(TokenType.OPEN_BLOCK); - const children = parseNestedTokens(); - - branches.push({ type: 'else', expression: null, children }); - need(TokenType.CLOSE_BLOCK); - } - - return { - type: TemplateNodeType.IF_STATEMENT, - branches, - }; - } - - function parseForStatement(_token: Token): ForStatementNode { - const expression = need(TokenType.EXPRESSION).substring; - need(TokenType.OPEN_BLOCK); - - const children = parseNestedTokens(); - need(TokenType.CLOSE_BLOCK); - - return { - type: TemplateNodeType.FOR_STATEMENT, - expression, - children, - }; - } - - function parseSwitchStatement(_token: Token): SwitchStatementNode { - const expression = need(TokenType.EXPRESSION).substring; - need(TokenType.OPEN_BLOCK); - - const branches: (CaseBranch | DefaultBranch)[] = []; - - while (expect(TokenType.SWITCH_CASE_STATEMENT)) { - const expression = need(TokenType.EXPRESSION).substring; - need(TokenType.OPEN_BLOCK); - const children = parseNestedTokens(); - - branches.push({ type: 'case', expression, children }); - need(TokenType.CLOSE_BLOCK); - } - - if (expect(TokenType.SWITCH_DEFAULT_STATEMENT)) { - need(TokenType.OPEN_BLOCK); - const children = parseNestedTokens(); - - branches.push({ type: 'default', children }); - need(TokenType.CLOSE_BLOCK); - } - - need(TokenType.CLOSE_BLOCK); - - return { - type: TemplateNodeType.SWITCH_STATEMENT, - expression, - branches, - }; - } - - function need(tokenType: TokenType): Token { - const nextOfType = expect(tokenType); - - if (!nextOfType) { - throw new TemplateError( - `Expected token type '${tokenType}'.`, - tokens[cursor + 1].startIndex, - ); - } - - return nextOfType; - } - - function expect(tokenType: TokenType): Token | null { - const next = tokens[cursor + 1]; - - if (next.type !== tokenType) { - return null; - } - - cursor += 1; - return next; - } - - return nodes; -} diff --git a/packages/client-api/src/template-engine/token-parsing/switch-statement-node.model.ts b/packages/client-api/src/template-engine/token-parsing/switch-statement-node.model.ts deleted file mode 100644 index f5c317f8..00000000 --- a/packages/client-api/src/template-engine/token-parsing/switch-statement-node.model.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { TemplateNodeType } from './template-node-type.model'; -import type { TemplateNode } from './template-node.model'; - -export interface CaseBranch { - type: 'case'; - expression: string; - children: TemplateNode[]; -} - -export interface DefaultBranch { - type: 'default'; - children: TemplateNode[]; -} - -export interface SwitchStatementNode { - type: TemplateNodeType.SWITCH_STATEMENT; - expression: string; - branches: (CaseBranch | DefaultBranch)[]; -} diff --git a/packages/client-api/src/template-engine/token-parsing/template-node-type.model.ts b/packages/client-api/src/template-engine/token-parsing/template-node-type.model.ts deleted file mode 100644 index 013900ac..00000000 --- a/packages/client-api/src/template-engine/token-parsing/template-node-type.model.ts +++ /dev/null @@ -1,7 +0,0 @@ -export enum TemplateNodeType { - TEXT = 'TEXT', - INTERPOLATION = 'INTERPOLATION', - IF_STATEMENT = 'IF_STATEMENT', - FOR_STATEMENT = 'FOR_STATEMENT', - SWITCH_STATEMENT = 'SWITCH_STATEMENT', -} diff --git a/packages/client-api/src/template-engine/token-parsing/template-node.model.ts b/packages/client-api/src/template-engine/token-parsing/template-node.model.ts deleted file mode 100644 index ea860a18..00000000 --- a/packages/client-api/src/template-engine/token-parsing/template-node.model.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { SwitchStatementNode } from './switch-statement-node.model'; -import type { ForStatementNode } from './for-statement-node'; -import type { IfStatementNode } from './if-statement-node.model'; -import type { InterpolationNode } from './interpolation-node.model'; -import type { TextNode } from './text-node.model'; - -export type TemplateNode = - | TextNode - | InterpolationNode - | IfStatementNode - | ForStatementNode - | SwitchStatementNode; diff --git a/packages/client-api/src/template-engine/token-parsing/text-node.model.ts b/packages/client-api/src/template-engine/token-parsing/text-node.model.ts deleted file mode 100644 index f0f32a33..00000000 --- a/packages/client-api/src/template-engine/token-parsing/text-node.model.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { TemplateNodeType } from './template-node-type.model'; - -export interface TextNode { - type: TemplateNodeType.TEXT; - text: string; -} diff --git a/packages/client-api/src/template-engine/tokenizing/index.ts b/packages/client-api/src/template-engine/tokenizing/index.ts deleted file mode 100644 index 60b22945..00000000 --- a/packages/client-api/src/template-engine/tokenizing/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './token-type.model'; -export * from './token.model'; -export * from './tokenize-template'; diff --git a/packages/client-api/src/template-engine/tokenizing/token-type.model.ts b/packages/client-api/src/template-engine/tokenizing/token-type.model.ts deleted file mode 100644 index fea9e587..00000000 --- a/packages/client-api/src/template-engine/tokenizing/token-type.model.ts +++ /dev/null @@ -1,40 +0,0 @@ -export enum TokenType { - /** Start of if statement (ie. `@if`). */ - IF_STATEMENT = 'IF_STATEMENT', - - /** Start of else if statement (ie. `@else if`). */ - ELSE_IF_STATEMENT = 'ELSE_IF_STATEMENT', - - /** Start of else statement (ie. `@else`). */ - ELSE_STATEMENT = 'ELSE_STATEMENT', - - /** Start of for statement (ie. `@for`). */ - FOR_STATEMENT = 'FOR_STATEMENT', - - /** Start of switch statement (ie. `@switch`). */ - SWITCH_STATEMENT = 'SWITCH_STATEMENT', - - /** Start of switch case statement (ie. `@case`). */ - SWITCH_CASE_STATEMENT = 'SWITCH_CASE_STATEMENT', - - /** Start of switch default statement (ie. `@default`). */ - SWITCH_DEFAULT_STATEMENT = 'SWITCH_DEFAULT_STATEMENT', - - /** Opening curly brace (ie. `{`) after the start of a statement. */ - OPEN_BLOCK = 'OPEN_BLOCK', - - /** Closing curly brace (ie. `}`) marking the end of a statement. */ - CLOSE_BLOCK = 'CLOSE_BLOCK', - - /** Opening double curly brace (ie. `{{`) of an interpolation tag. */ - OPEN_INTERPOLATION = 'OPEN_INTERPOLATION', - - /** Closing double curly brace (ie. `}}`) of an interpolation tag. */ - CLOSE_INTERPOLATION = 'CLOSE_INTERPOLATION', - - /** Expression to evaluate (within tag start or an interpolation tag. */ - EXPRESSION = 'EXPRESSION', - - /** Ordinary text. */ - TEXT = 'TEXT', -} diff --git a/packages/client-api/src/template-engine/tokenizing/token.model.ts b/packages/client-api/src/template-engine/tokenizing/token.model.ts deleted file mode 100644 index 8f740382..00000000 --- a/packages/client-api/src/template-engine/tokenizing/token.model.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { TokenType } from './token-type.model'; - -export interface Token { - type: TokenType; - substring: string; - startIndex: number; - endIndex: number; -} diff --git a/packages/client-api/src/template-engine/tokenizing/tokenize-template.ts b/packages/client-api/src/template-engine/tokenizing/tokenize-template.ts deleted file mode 100644 index 68c60b17..00000000 --- a/packages/client-api/src/template-engine/tokenizing/tokenize-template.ts +++ /dev/null @@ -1,277 +0,0 @@ -import { createStringScanner } from '~/utils'; -import type { Token } from './token.model'; -import { TokenType } from './token-type.model'; -import { TemplateError } from '../shared'; - -export enum TokenizeStateType { - DEFAULT, - IN_STATEMENT_ARGS, - IN_STATEMENT_BLOCK, - IN_INTERPOLATION, - IN_EXPRESSION, -} - -export interface InExpressionState { - type: TokenizeStateType.IN_EXPRESSION; - token?: Token; - closeRegex: RegExp; - activeWrappingSymbol: string | null; -} - -export type TokenizeState = - | { type: TokenizeStateType } - | InExpressionState; - -export function tokenizeTemplate(template: string): Token[] { - // Stack of tokenize states. Last element represents current state. - const stateStack: TokenizeState[] = [ - { type: TokenizeStateType.DEFAULT }, - ]; - - // Tokens within input template. - const tokens: Token[] = []; - - // String scanner for advancing through input template. - const scanner = createStringScanner(template); - - function pushToken(typeOrToken: TokenType | Token) { - const token = - typeof typeOrToken === 'object' - ? typeOrToken - : { type: typeOrToken, ...scanner.latestMatch! }; - - // Skip pushing empty tokens. - if (token.substring) { - tokens.push(token); - } - } - - // Push a tokenize state. - function pushState(typeOrState: TokenizeStateType | TokenizeState) { - const state = - typeof typeOrState === 'object' - ? typeOrState - : { type: typeOrState }; - - stateStack.push(state); - } - - function updateLatestState(state: Partial) { - if (!stateStack.length) { - throw new TemplateError( - 'Could not update latest state while tokenizing template.', - scanner.cursor, - ); - } - - stateStack[stateStack.length - 1] = { - ...stateStack[stateStack.length - 1]!, - ...state, - }; - } - - // Get current tokenize state. - function getState() { - return stateStack[stateStack.length - 1]; - } - - while (!scanner.isEmpty) { - switch (getState()!.type) { - case TokenizeStateType.DEFAULT: - tokenizeDefault(); - break; - case TokenizeStateType.IN_STATEMENT_ARGS: - tokenizeStatementArgs(); - break; - case TokenizeStateType.IN_STATEMENT_BLOCK: - tokenizeStatementBlock(); - break; - case TokenizeStateType.IN_INTERPOLATION: - tokenizeInterpolation(); - break; - case TokenizeStateType.IN_EXPRESSION: - tokenizeExpression(); - break; - } - } - - function tokenizeDefault() { - if (scanner.scan(/\s*@if/)) { - pushToken(TokenType.IF_STATEMENT); - pushState(TokenizeStateType.IN_STATEMENT_ARGS); - } else if (scanner.scan(/\s*@else\s+if/)) { - pushToken(TokenType.ELSE_IF_STATEMENT); - pushState(TokenizeStateType.IN_STATEMENT_ARGS); - } else if (scanner.scan(/\s*@else/)) { - pushToken(TokenType.ELSE_STATEMENT); - pushState(TokenizeStateType.IN_STATEMENT_ARGS); - } else if (scanner.scan(/\s*@for/)) { - pushToken(TokenType.FOR_STATEMENT); - pushState(TokenizeStateType.IN_STATEMENT_ARGS); - } else if (scanner.scan(/\s*@switch/)) { - pushToken(TokenType.SWITCH_STATEMENT); - pushState(TokenizeStateType.IN_STATEMENT_ARGS); - } else if (scanner.scan(/\s*@case/)) { - pushToken(TokenType.SWITCH_CASE_STATEMENT); - pushState(TokenizeStateType.IN_STATEMENT_ARGS); - } else if (scanner.scan(/\s*@default/)) { - pushToken(TokenType.SWITCH_DEFAULT_STATEMENT); - pushState(TokenizeStateType.IN_STATEMENT_ARGS); - } else if (scanner.scan(/(?:[\n\r\v\f\t]+\s*|){{/)) { - // Ignore newlines before interpolation tag. Spaces are not ignored - // for when the interpolation tag is mixed in with text tokens. - pushToken(TokenType.OPEN_INTERPOLATION); - pushState(TokenizeStateType.IN_INTERPOLATION); - } else if (scanner.scanUntil(/[\S\s]+?(?={{|@|\})/)) { - // Search until a close block, the start of a statement, or the start - // of an interpolation tag. In a JavaScript regex, the `.` character - // does not match newline characters, so we instead use [\S\s]. - pushToken({ - type: TokenType.TEXT, - ...scanner.latestMatch!, - }); - } else { - throw new TemplateError('No valid tokens found.', scanner.cursor); - } - } - - function tokenizeStatementArgs() { - if (scanner.scan(/\)?\s+/)) { - // Ignore the closing parenthesis and any following whitespace. - } else if (scanner.scan(/\(/)) { - pushState({ - type: TokenizeStateType.IN_EXPRESSION, - closeRegex: /[\S\s]+?(?=\))/, - activeWrappingSymbol: null, - }); - } else if (scanner.scan(/{\s*/)) { - pushToken(TokenType.OPEN_BLOCK); - stateStack.pop(); - pushState(TokenizeStateType.IN_STATEMENT_BLOCK); - } else { - throw new TemplateError( - 'Missing closing { after statement.', - scanner.cursor, - ); - } - } - - function tokenizeStatementBlock() { - if (scanner.scan(/}\s*/)) { - pushToken(TokenType.CLOSE_BLOCK); - stateStack.pop(); - } else { - tokenizeDefault(); - } - } - - function tokenizeInterpolation() { - if (scanner.scan(/\s+/)) { - // Ignore whitespace within interpolation tag. - } else if (scanner.scan(/}}(?:[\n\r\v\f\t]+\s*|)/)) { - pushToken(TokenType.CLOSE_INTERPOLATION); - stateStack.pop(); - } else if (scanner.scan(/[\S\s]*?/)) { - pushState({ - type: TokenizeStateType.IN_EXPRESSION, - closeRegex: /[\S\s]+?(?=}})/, - activeWrappingSymbol: null, - }); - } else { - throw new TemplateError( - 'Missing closing }} after expression.', - scanner.cursor, - ); - } - } - - function tokenizeExpression() { - const state = getState() as InExpressionState; - - if (scanner.scan(/\s+/)) { - // Ignore whitespace within expression. - } else if (scanner.scan(state.closeRegex)) { - const { startIndex, endIndex, substring } = scanner.latestMatch!; - - // String scanner for finding wrapping symbols within the matched - // substring. The closing symbol should be ignored if wrapped within an - // unclosed string or parenthesis. - const subScanner = createStringScanner(substring); - let activeWrappingSymbol = state.activeWrappingSymbol; - - while (!subScanner.isEmpty) { - const symbolMatch = subScanner.scan(/[\S\s]*?('|`|\(|\)|")/); - - if (!symbolMatch) { - break; - } - - // Get last character of scanned string (either (, ), ', ", or `). - const foundSymbol = symbolMatch.substring.trimEnd().slice(-1); - - activeWrappingSymbol = getActiveWrappingSymbol( - activeWrappingSymbol, - foundSymbol, - ); - } - - // If there's an active wrapping symbol, update the token created thus - // far, and continue scanning. - if (activeWrappingSymbol) { - updateLatestState({ - activeWrappingSymbol, - token: { - type: TokenType.EXPRESSION, - startIndex: state.token?.startIndex ?? startIndex, - endIndex, - substring: (state.token?.substring ?? '') + substring, - }, - }); - return; - } - - pushToken({ - type: TokenType.EXPRESSION, - startIndex: state.token?.startIndex ?? startIndex, - endIndex, - substring: (state.token?.substring ?? '') + substring.trimEnd(), - }); - - stateStack.pop(); - } else { - throw new TemplateError( - 'Missing close symbol after expression.', - scanner.cursor, - ); - } - } - - function getActiveWrappingSymbol( - current: string | null, - matched: string, - ) { - const isOpeningSymbol = matched !== ')'; - - // Set active wrapping symbol to the matched symbol. - if (!current && isOpeningSymbol) { - return matched; - } - - const inverse = current === '(' ? ')' : current; - - // Otherwise, set/clear wrapping symbol depending on whether the inverse - // symbol matches. - return matched === inverse ? null : current; - } - - // If state stack includes more than just the default state, then a - // statement or expression has no closing tag. - if (stateStack.length > 1) { - throw new TemplateError( - 'Missing close symbol after statement or expression.', - scanner.cursor, - ); - } - - return tokens; -} diff --git a/packages/client-api/src/user-config/get-parsed-element-config.ts b/packages/client-api/src/user-config/get-parsed-element-config.ts deleted file mode 100644 index 3da156bc..00000000 --- a/packages/client-api/src/user-config/get-parsed-element-config.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { type Owner, createComputed, runWithOwner } from 'solid-js'; -import { createStore } from 'solid-js/store'; - -import type { ElementContext } from '~/element-context.model'; -import { ElementType } from '~/element-type.model'; -import { TemplateError, getTemplateEngine } from '~/template-engine'; -import { - GroupConfigSchemaP1, - TemplateConfigSchema, - WindowConfigSchemaP1, - parseWithSchema, -} from '~/user-config'; -import type { PickPartial } from '~/utils'; - -export function getParsedElementConfig( - elementContext: PickPartial, - owner: Owner, -) { - const templateEngine = getTemplateEngine(); - - const [parsedConfig, setParsedConfig] = createStore(getParsedConfig()); - - // Update the store on changes to any provider variables. - runWithOwner(owner, () => { - createComputed(() => setParsedConfig(getParsedConfig())); - }); - - /** - * Get updated store value. - */ - function getParsedConfig() { - const config = { - ...(elementContext.rawConfig as Record), - id: elementContext.id, - }; - - const schema = getSchemaForElement(elementContext.type); - - const newConfigEntries = Object.entries(config).map(([key, value]) => { - // If value is not a string, then it can't contain any templating syntax. - if (typeof value !== 'string') { - return [key, value]; - } - - // Run the value through the templating engine. - try { - const rendered = templateEngine.render( - value, - elementContext.providers, - ); - - return [key, rendered]; - } catch (err) { - if (!(err instanceof TemplateError)) { - throw err; - } - - const { message, templateIndex } = err; - - throw new Error( - `Property '${key}' in config isn't valid.\n\n` + - 'Syntax error at:\n' + - `...${value.slice(templateIndex - 30, templateIndex)} << \n\n` + - `⚠️ ${message}`, - ); - } - }); - - // TODO: Add logging for updated config here. - const newConfig = Object.fromEntries(newConfigEntries); - - return parseWithSchema(schema, newConfig); - } - - return parsedConfig; -} - -// TODO: Validate in P1 schemas that `template/` and `group/` keys exist. -function getSchemaForElement(type: ElementType) { - switch (type) { - case ElementType.WINDOW: - return WindowConfigSchemaP1.strip(); - case ElementType.GROUP: - return GroupConfigSchemaP1.strip(); - case ElementType.TEMPLATE: - return TemplateConfigSchema.strip(); - } -} diff --git a/packages/client-api/src/user-config/get-script-manager.ts b/packages/client-api/src/user-config/get-script-manager.ts deleted file mode 100644 index 33019e87..00000000 --- a/packages/client-api/src/user-config/get-script-manager.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { convertFileSrc } from '@tauri-apps/api/core'; -import { join, homeDir } from '@tauri-apps/api/path'; - -import { createLogger } from '~/utils'; -import type { ElementContext } from '../element-context.model'; - -const logger = createLogger('script-manager'); - -/** - * Map of module paths to asset paths. - */ -const assetPathCache: Record = {}; - -/** - * Map of asset paths to promises that resolve to the module. - */ -const moduleCache: Record> = {}; - -/** - * Abstraction over loading and invoking user-defined scripts. - */ -export function getScriptManager() { - return { - loadScriptForFn, - callFn, - }; -} - -async function loadScriptForFn(fnPath: string): Promise { - const { modulePath } = parseFnPath(fnPath); - return resolveModule(modulePath); -} - -async function callFn( - fnPath: string, - event: Event, - context: ElementContext, -): Promise { - const { modulePath, functionName } = parseFnPath(fnPath); - 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); -} - -async function resolveModule(modulePath: string): Promise { - const assetPath = await getAssetPath(modulePath); - const foundModule = moduleCache[assetPath]; - - if (foundModule) { - return foundModule; - } - - logger.info(`Loading script at path '${assetPath}'.`); - return (moduleCache[assetPath] = import(/* @vite-ignore */ assetPath)); -} - -/** - * 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 (assetPathCache[modulePath] = convertFileSrc( - await join(await homeDir(), '.glzr/zebar', modulePath), - )); -} - -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/get-style-builder.ts b/packages/client-api/src/user-config/get-style-builder.ts deleted file mode 100644 index 7bc09c13..00000000 --- a/packages/client-api/src/user-config/get-style-builder.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { createLogger, toCssSelector } from '~/utils'; - -const logger = createLogger('style-builder'); - -let globalStyles: string | null = null; -let elementStyles: Record = {}; -let styleElement: HTMLStyleElement | null = null; - -/** - * Abstraction over building CSS from user-defined styles. - */ -export function getStyleBuilder() { - function buildGlobalStyles(styles: string) { - logger.debug(`Updating global CSS:`, styles); - - globalStyles = styles; - buildStyles(); - } - - function buildElementStyles(id: string, styles: string) { - // Wrap user-defined styles in a scope. - const scopedStyles = `#${toCssSelector(id)} {\n${styles}}`; - logger.debug(`Updating element '${id}' CSS:\n`, scopedStyles); - - elementStyles[id] = scopedStyles; - buildStyles(); - } - - /** - * Compile user-defined CSS and add it to the DOM. - */ - function buildStyles() { - if (!styleElement) { - styleElement = document.createElement('style'); - styleElement.setAttribute('data-zebar', ''); - document.head.appendChild(styleElement); - } - - const styles = [globalStyles ?? '', ...Object.values(elementStyles)]; - styleElement.innerHTML = styles.join('\n'); - } - - return { - buildGlobalStyles, - buildElementStyles, - }; -} diff --git a/packages/client-api/src/user-config/get-user-config.ts b/packages/client-api/src/user-config/get-user-config.ts deleted file mode 100644 index 49cec245..00000000 --- a/packages/client-api/src/user-config/get-user-config.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { createSignal } from 'solid-js'; -import { parse } from 'yaml'; - -import { createLogger } from '~/utils'; -import { readConfigFile } from '~/desktop'; - -const logger = createLogger('get-user-config'); - -/** - * User config (if read) as parsed YAML. - */ -const [userConfig, setUserConfig] = createSignal(null); - -/** - * Get user config as parsed YAML. - */ -export async function getUserConfig() { - if (userConfig()) { - return userConfig(); - } - - try { - const configStr = await readConfigFile(); - const configObj = parse(configStr) as unknown; - setUserConfig(configObj); - - logger.debug(`Read config:`, configObj); - - return configObj; - } catch (err) { - throw new Error( - `Problem reading config file. ${(err as Error).message}`, - ); - } -} diff --git a/packages/client-api/src/user-config/global-config.model.ts b/packages/client-api/src/user-config/global-config.model.ts deleted file mode 100644 index e20609ae..00000000 --- a/packages/client-api/src/user-config/global-config.model.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { z } from 'zod'; - -import { BooleanLikeSchema } from './shared'; - -export const GlobalConfigSchema = z - .object({ - enable_devtools: BooleanLikeSchema.default(false), - }) - .partial(); - -export type GlobalConfig = z.infer; diff --git a/packages/client-api/src/user-config/index.ts b/packages/client-api/src/user-config/index.ts index 51522251..d2673b74 100644 --- a/packages/client-api/src/user-config/index.ts +++ b/packages/client-api/src/user-config/index.ts @@ -1,9 +1 @@ -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'; -export * from './parse-with-schema'; -export * from './shared'; -export * from './user-config.model'; -export * from './window'; +export * from './window-config'; diff --git a/packages/client-api/src/user-config/parse-with-schema.ts b/packages/client-api/src/user-config/parse-with-schema.ts deleted file mode 100644 index b9f5e99c..00000000 --- a/packages/client-api/src/user-config/parse-with-schema.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ZodError, type z } from 'zod'; - -/** - * Parse a value with error formatting. - */ -export function parseWithSchema( - schema: T, - value: unknown, -): z.infer { - try { - return schema.parse(value); - } catch (err) { - if (err instanceof ZodError && err.errors.length) { - const [firstError] = err.errors; - const { message, path } = firstError!; - const fullPath = path.join('.'); - - throw new Error( - `Property '${fullPath}' in config isn't valid.\n` + - `⚠️ ${message}`, - ); - } - - throw new Error(`Failed to parse config. ${(err as Error).message}`); - } -} diff --git a/packages/client-api/src/user-config/shared/boolean-like.model.ts b/packages/client-api/src/user-config/shared/boolean-like.model.ts deleted file mode 100644 index b0f97f3f..00000000 --- a/packages/client-api/src/user-config/shared/boolean-like.model.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { z } from 'zod'; - -export const BooleanLikeSchema = z - .union([z.boolean(), z.literal('true'), z.literal('false')]) - .transform(value => value === true || value === 'true'); diff --git a/packages/client-api/src/user-config/shared/get-child-configs.ts b/packages/client-api/src/user-config/shared/get-child-configs.ts deleted file mode 100644 index e30ecf73..00000000 --- a/packages/client-api/src/user-config/shared/get-child-configs.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { ElementConfig } from '~/element-context.model'; -import type { GroupConfig, TemplateConfig } from '../window'; -import type { ElementType } from '~/element-type.model'; - -export interface ChildConfigRef { - type: ElementType; - id: string; - config: TemplateConfig | GroupConfig; -} - -/** - * Get template and group configs within {@link rawConfig}. - */ -export function getChildConfigs(rawConfig: unknown): ChildConfigRef[] { - return Object.entries(rawConfig as ElementConfig).reduce< - ChildConfigRef[] - >((acc, [key, value]) => { - const childKeyRegex = /^(template|group)\/(.+)$/; - const match = key.match(childKeyRegex); - - if (!match) { - return acc; - } - - return [ - ...acc, - { - type: match[1], - id: match[2], - config: value, - } as ChildConfigRef, - ]; - }, []); -} diff --git a/packages/client-api/src/user-config/shared/index.ts b/packages/client-api/src/user-config/shared/index.ts deleted file mode 100644 index 7bb6ea76..00000000 --- a/packages/client-api/src/user-config/shared/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './boolean-like.model'; -export * from './get-child-configs'; -export * from './with-dynamic-key'; diff --git a/packages/client-api/src/user-config/shared/with-dynamic-key.ts b/packages/client-api/src/user-config/shared/with-dynamic-key.ts deleted file mode 100644 index 59a02e5b..00000000 --- a/packages/client-api/src/user-config/shared/with-dynamic-key.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { z } from 'zod'; - -export type WithDynamicKeyOptions< - TKey extends string, - TSub extends z.ZodType, -> = { - isKey: (key: string) => key is TKey; - schema: TSub; -}; - -/** - * Adds a dynamic key to an object schema. - * - * @example - * ```typescript - * const MySchema = withDynamicKey(z.object({}), { - * isKey: (key: string): key is `sub/${string}` => key.startsWith('sub/'), - * schema: SubSchema, - * }); // {} & { x: `sub/${string}`]: SubSchema } - * ``` - */ -export function withDynamicKey< - TObject extends z.AnyZodObject, - const TKey extends string, - TSub extends z.ZodType, ->(schema: TObject, options: WithDynamicKeyOptions) { - // `passthrough` is needed here to allow the dynamic key. The resulting type - // is then narrowed down and validated within `transform`. - return schema.passthrough().transform((val, ctx) => { - const defaultKeys = Object.keys(schema.shape); - - for (const key of Object.keys(val)) { - if (defaultKeys.includes(key)) { - continue; - } - - if (!options.isKey(key)) { - ctx.addIssue({ - code: z.ZodIssueCode.unrecognized_keys, - keys: [key], - path: [key], - }); - } - } - - const dynamicKeys = Object.keys(val).filter(key => options.isKey(key)); - - for (const dynamicKey of dynamicKeys) { - const res = options.schema.safeParse(val[dynamicKey]); - - if (res.success) { - val[dynamicKey] = res.data; - continue; - } - - for (const issue of res.error.issues) { - ctx.addIssue({ - ...issue, - path: [dynamicKey, ...issue.path], - }); - } - } - - return val as z.infer & { - [key in TKey]: z.infer; - }; - }); -} diff --git a/packages/client-api/src/user-config/user-config.model.ts b/packages/client-api/src/user-config/user-config.model.ts deleted file mode 100644 index edd5b6fc..00000000 --- a/packages/client-api/src/user-config/user-config.model.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { z } from 'zod'; - -import { WindowConfigSchema } from './window'; -import { GlobalConfigSchema } from './global-config.model'; -import { withDynamicKey } from './shared'; -import type { Prettify } from '~/utils'; - -export const UserConfigP1Schema = z.object({ - global: GlobalConfigSchema, -}); - -export type UserConfigP1 = Prettify>; - -// Add `window/**` keys to schema. -export const UserConfigSchema = withDynamicKey(UserConfigP1Schema, { - isKey: (key: string): key is `window/${string}` => - key.startsWith('window/'), - schema: WindowConfigSchema, -}); - -export type UserConfig = Prettify>; diff --git a/packages/client-api/src/user-config/window-config.ts b/packages/client-api/src/user-config/window-config.ts new file mode 100644 index 00000000..2a120393 --- /dev/null +++ b/packages/client-api/src/user-config/window-config.ts @@ -0,0 +1,66 @@ +export interface WindowConfig { + /** + * Whether to show the window above/below all others. + */ + z_order: WindowZOrder; + + /** + * Whether the window should be shown in the taskbar. + */ + shown_in_taskbar: boolean; + + /** + * Whether the window should have resize handles. + */ + resizable: boolean; + + /** + * Whether the window is transparent. + */ + transparent: boolean; + + /** + * Entry point HTML file for the window. + */ + html_path: string; + + /** + * Where to place the window. + */ + placements: WindowPlacement[]; +} + +export enum WindowZOrder { + ALWAYS_ON_BOTTOM = 'always_on_bottom', + ALWAYS_ON_TOP = 'always_on_top', + NORMAL = 'normal', +} + +export interface WindowPlacement { + /** + * The monitor index to place the window on. + */ + monitor_index: 0; + + /** + * TODO: Add description. + */ + position: WindowPosition; + + /** + * TODO: Add description. + */ + offset_x: 20; + + /** + * TODO: Add description. + */ + offset_y: 20; +} + +export enum WindowPosition { + TOP = 'top', + BOTTOM = 'bottom', + LEFT = 'left', + RIGHT = 'right', +} 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 deleted file mode 100644 index f5ff4690..00000000 --- a/packages/client-api/src/user-config/window/base-element-config.model.ts +++ /dev/null @@ -1,18 +0,0 @@ -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. */ -export type BaseElementConfig = Prettify< - z.infer ->; 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 deleted file mode 100644 index bcd6bef2..00000000 --- a/packages/client-api/src/user-config/window/element-events-config.model.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { z } from 'zod'; -import type { Prettify } from '~/utils'; - -/** - * All available events on HTML elements. - **/ -const HTML_EVENTS = [ - '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 - .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'.", - ), - selector: z.string().optional(), - }), - ) - .default([]); - -export type ElementEventsConfig = Prettify< - z.infer ->; diff --git a/packages/client-api/src/user-config/window/group-config.model.ts b/packages/client-api/src/user-config/window/group-config.model.ts deleted file mode 100644 index 665f3eca..00000000 --- a/packages/client-api/src/user-config/window/group-config.model.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { z } from 'zod'; - -import { TemplateConfigSchema } from './template-config.model'; -import { BaseElementConfigSchema } from './base-element-config.model'; -import type { Prettify } from '~/utils'; -import { withDynamicKey } from '../shared'; - -export const GroupConfigSchemaP1 = BaseElementConfigSchema.extend({ - class_names: z.array(z.string()).default(['group']), -}); - -export type GroupConfigP1 = Prettify>; - -// Add `template/**` keys to schema. -export const GroupConfigSchema = withDynamicKey(GroupConfigSchemaP1, { - isKey: (key: string): key is `template/${string}` => - key.startsWith('template/'), - schema: TemplateConfigSchema, -}); - -export type GroupConfig = Prettify>; diff --git a/packages/client-api/src/user-config/window/index.ts b/packages/client-api/src/user-config/window/index.ts index 937e359a..96c93c71 100644 --- a/packages/client-api/src/user-config/window/index.ts +++ b/packages/client-api/src/user-config/window/index.ts @@ -1,10 +1,3 @@ -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'; export * from './providers'; -export * from './providers-config.model'; -export * from './template-config.model'; -export * from './window-config.model'; -export * from './z-order.model'; diff --git a/packages/client-api/src/user-config/window/providers-config.model.ts b/packages/client-api/src/user-config/window/providers-config.model.ts deleted file mode 100644 index 93edd821..00000000 --- a/packages/client-api/src/user-config/window/providers-config.model.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { z } from 'zod'; - -import { ProviderConfigSchema } from './provider-config.model'; -import { ProviderTypeSchema } from './provider-type.model'; -import type { Prettify } from '~/utils'; - -export const ProvidersConfigSchema = z - .array( - z.union([ - ProviderConfigSchema, - ProviderTypeSchema.transform(type => - ProviderConfigSchema.parse({ type }), - ), - ]), - ) - .default([]); - -export type ProvidersConfig = Prettify< - z.infer ->; diff --git a/packages/client-api/src/user-config/window/template-config.model.ts b/packages/client-api/src/user-config/window/template-config.model.ts deleted file mode 100644 index ca2b2ee8..00000000 --- a/packages/client-api/src/user-config/window/template-config.model.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { z } from 'zod'; - -import { BaseElementConfigSchema } from './base-element-config.model'; - -export const TemplateConfigSchema = BaseElementConfigSchema.extend({ - class_names: z.array(z.string()).default(['template']), - template: z.string(), -}); - -export type TemplateConfig = z.infer; diff --git a/packages/client-api/src/user-config/window/window-config.model.ts b/packages/client-api/src/user-config/window/window-config.model.ts deleted file mode 100644 index 3d884794..00000000 --- a/packages/client-api/src/user-config/window/window-config.model.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { z } from 'zod'; - -import { GroupConfigSchema } from './group-config.model'; -import { BaseElementConfigSchema } from './base-element-config.model'; -import type { Prettify } from '~/utils'; -import { BooleanLikeSchema, withDynamicKey } from '../shared'; -import { ZOrderSchema } from './z-order.model'; - -export const WindowConfigSchemaP1 = BaseElementConfigSchema.extend({ - class_names: z.array(z.string()).default(['window']), - position_x: z.coerce.number(), - position_y: z.coerce.number(), - width: z.coerce.number().min(1), - height: z.coerce.number().min(1), - z_order: ZOrderSchema, - // TODO: Deprecate in future release in favour of `shown_in_taskbar`. - show_in_taskbar: BooleanLikeSchema.optional(), - shown_in_taskbar: BooleanLikeSchema.optional(), - resizable: BooleanLikeSchema.optional(), - global_styles: z.string().optional(), -}); - -export type WindowConfigP1 = Prettify< - z.infer ->; - -// Add `group/**` keys to schema. -// TODO: Should be able to have `template/` as a child of window config. -export const WindowConfigSchema = withDynamicKey(WindowConfigSchemaP1, { - isKey: (key: string): key is `group/${string}` => - key.startsWith('group/'), - schema: GroupConfigSchema, -}); - -export type WindowConfig = Prettify>; diff --git a/packages/client-api/src/user-config/window/z-order.model.ts b/packages/client-api/src/user-config/window/z-order.model.ts deleted file mode 100644 index 903749b5..00000000 --- a/packages/client-api/src/user-config/window/z-order.model.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { z } from 'zod'; - -export const ZOrderSchema = z - .enum(['always_on_top', 'always_on_bottom', 'normal']) - .default('normal'); - -export type ZOrder = z.infer; diff --git a/packages/client-api/src/utils/clsx.ts b/packages/client-api/src/utils/clsx.ts deleted file mode 100644 index 6eed8a5b..00000000 --- a/packages/client-api/src/utils/clsx.ts +++ /dev/null @@ -1,38 +0,0 @@ -type ClassValue = - | string - | string[] - | Record - | null - | undefined; - -/** - * Utility for constructing `class` names conditionally. - * Inspired by `clsx` https://github.com/lukeed/clsx. - */ -export function clsx(...inputs: ClassValue[]): string { - let classString = ''; - - for (const input of inputs) { - if (input === null || input === undefined) { - continue; - } - - if (typeof input === 'string') { - classString += `${input} `; - } - - if (Array.isArray(input)) { - input.forEach(inputPart => (classString += `${inputPart} `)); - } - - if (typeof input === 'object') { - for (const [key, val] of Object.entries(input)) { - if (!!val) { - classString += `${key} `; - } - } - } - } - - return classString; -} diff --git a/packages/client-api/src/utils/create-getter-proxy.ts b/packages/client-api/src/utils/create-getter-proxy.ts deleted file mode 100644 index 97bb195b..00000000 --- a/packages/client-api/src/utils/create-getter-proxy.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Wraps a target object and deeply tracks property access. - * - * @param target Object to wrap. - * @param callback Invoked on every property access with an array of keys to - * the accessed value. - */ -export function createGetterProxy( - target: T, - callback: (path: (string | symbol)[]) => void, -): T { - // Proxy cache is used to avoid creating a new proxy when a property is - // accessed repeatedly. - const proxyCache = new WeakMap(); - - function wrap( - target: U, - parentPath: (string | symbol)[], - ): U { - if (proxyCache.has(target)) { - return proxyCache.get(target); - } - - const proxy = new Proxy(target, { - get(target, key, receiver) { - const value = Reflect.get(target, key, receiver); - - // Invoke callback with the path to the accessed key. - const path = [...parentPath, key]; - callback(path); - - if (typeof value === 'object' && value !== null) { - return wrap(value, path); - } - - return value; - }, - }); - - proxyCache.set(proxy, target); - return proxy; - } - - return wrap(target, []); -} diff --git a/packages/client-api/src/utils/create-string-scanner.ts b/packages/client-api/src/utils/create-string-scanner.ts deleted file mode 100644 index 28bb4315..00000000 --- a/packages/client-api/src/utils/create-string-scanner.ts +++ /dev/null @@ -1,65 +0,0 @@ -export interface ScannerMatch { - substring: string; - endIndex: number; - startIndex: number; -} - -/** - * Utility for advancing through an input string via regex. - */ -export function createStringScanner(input: string) { - let cursor = 0; - let remainder = input; - let latestMatch: ScannerMatch | null = null; - - // Set `latestMatch` and advance the cursor accordingly. - function setLatestMatch(matchIndex: number, matchLength: number) { - const originalCursor = cursor; - remainder = remainder.substring(matchIndex + matchLength); - cursor += matchIndex + matchLength; - - return (latestMatch = { - substring: input.substring(originalCursor, cursor), - endIndex: cursor, - startIndex: originalCursor, - }); - } - - // If the regex matches at the *current* cursor position, set latest match - // and advance the cursor. - function scan(regex: RegExp): ScannerMatch | null { - const match = regex.exec(remainder); - - return match?.index !== 0 - ? null - : setLatestMatch(match.index, match[0].length); - } - - // If the regex matches at any of the remaining input, set latest match and - // advance the cursor. If there are no matches, advance the cursor to end of - // input. - function scanUntil(regex: RegExp): ScannerMatch { - const match = regex.exec(remainder); - - return match - ? setLatestMatch(match.index, match[0].length) - : setLatestMatch(0, remainder.length); - } - - return { - get cursor() { - return cursor; - }, - get remainder() { - return remainder; - }, - get latestMatch() { - return latestMatch; - }, - get isEmpty() { - return remainder === ''; - }, - scan, - scanUntil, - }; -} diff --git a/packages/client-api/src/utils/index.ts b/packages/client-api/src/utils/index.ts index 85438cd5..b1cc1cf2 100644 --- a/packages/client-api/src/utils/index.ts +++ b/packages/client-api/src/utils/index.ts @@ -1,9 +1,4 @@ -export * from './types/pick-partial'; export * from './types/prettify'; -export * from './clsx'; -export * from './create-getter-proxy'; export * from './create-logger'; -export * from './create-string-scanner'; export * from './get-coordinate-distance'; export * from './simple-hash'; -export * from './to-css-selector'; diff --git a/packages/client-api/src/utils/to-css-selector.ts b/packages/client-api/src/utils/to-css-selector.ts deleted file mode 100644 index 20490cd3..00000000 --- a/packages/client-api/src/utils/to-css-selector.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Convert a string to a valid CSS selector. - */ -export function toCssSelector(input: string): string { - // Replace non-alphanumeric characters with hyphens. - const sanitizedInput = input.replace(/[^a-zA-Z0-9]/g, '-'); - - // Ensure the selector doesn't start with a number. - return /^\d/.test(sanitizedInput) - ? `_${sanitizedInput}` - : sanitizedInput; -} diff --git a/packages/client-api/src/utils/types/pick-partial.ts b/packages/client-api/src/utils/types/pick-partial.ts deleted file mode 100644 index e33d5a80..00000000 --- a/packages/client-api/src/utils/types/pick-partial.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type PickPartial = Omit & - Partial>; diff --git a/packages/client-api/src/zebar-context.model.ts b/packages/client-api/src/zebar-context.model.ts new file mode 100644 index 00000000..99a43cb0 --- /dev/null +++ b/packages/client-api/src/zebar-context.model.ts @@ -0,0 +1,56 @@ +import { Window as TauriWindow } from '@tauri-apps/api/window'; + +import type { WindowConfig, WindowZOrder } from '~/user-config'; + +export interface ZebarContext { + /** + * Parsed window config. + */ + config: WindowConfig; + + /** + * Map of this element's providers and their variables. + */ + providers: TProviders; + + currentWindow: ZebarWindow; + + allWindows: ZebarWindow; + + currentMonitor: Monitor; + + allMonitors: Monitor; + + /** + * Opens a new window by a relative path to its config file. + */ + openWindow: ( + configPath: string, + args?: Record, + ) => Promise; +} + +export interface ZebarWindow { + tauri: TauriWindow; + setZOrder: (zOrder: WindowZOrder) => Promise; +} + +export interface Monitor { + /** Human-readable name of the monitor */ + name: string | null; + + /** Width of monitor in physical pixels. */ + width: number; + + /** Height of monitor in physical pixels. */ + height: number; + + /** X-coordinate of monitor in physical pixels. */ + x: number; + + /** Y-coordinate of monitor in physical pixels. */ + y: number; + + /** Scale factor to map physical pixels to logical pixels. */ + scaleFactor: number; +} diff --git a/packages/desktop/resources/draft.yaml b/packages/desktop/resources/draft.yaml new file mode 100644 index 00000000..e60b5b57 --- /dev/null +++ b/packages/desktop/resources/draft.yaml @@ -0,0 +1,48 @@ +# Allowed values: 'window', 'component' +type: 'window' +# Whether to show the window above/below all others. +# Allowed values: 'always_on_top', 'always_on_bottom', 'normal'. +z_order: 'normal' +# Whether the window should be shown in the taskbar. +shown_in_taskbar: false +# Whether the window should have resize handles. +resizable: false +html_url: 'index.html' +# Where to place the window. +positions: + # The monitor index to place the window on. + # Allowed values: 0, 1, 2, 3, etc. OR 'all'. + - monitor_index: 0 + # Allowed values: 'top', 'bottom', 'left', 'right'. + position: 'top' + offset_x: 20 + offset_y: 20 +inputs: + - type: 'number' + name: 'CPU high threshold' + value: 50 + - type: 'color' + name: 'background' + value: '#fff' + - type: 'component' + name: 'clock_component' + - type: 'component_list' + name: 'left_group' + value: + [ + { type: 'component', name: 'logo_component' }, + { type: 'component', name: 'glazewm_workspaces_component' }, + ] +# # Problems with current: +# # * start script (ideally someone just launches Zebar from start menu). +# # * reacting to monitor removals/additions. +# # * improvements to rendering. +# # * nested structure kind of sucks. a template file would be better (e.g. a `zhtml` file). +# # * however, a template file means re-rendering the entire template string. +# # * the more optimizations that we make, the closer we get to essentially creating a frontend framework. + +### Needed for MVP: +# 1. Config above without component support (including monitor positioning). +# 2. No UI. +# 3. Load windows via system tray. +# 4. Traverse yaml files in `~/.glzr/zebar` diff --git a/packages/desktop/resources/index.html b/packages/desktop/resources/index.html new file mode 100644 index 00000000..495ff786 --- /dev/null +++ b/packages/desktop/resources/index.html @@ -0,0 +1,20 @@ + + + + + From ed455eae1c1b9db380644030409cf7423fc91b28 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 22 Aug 2024 02:14:27 +0800 Subject: [PATCH 002/138] feat: create/use existing reactive solidjs context --- .../src/desktop/desktop-commands.ts | 2 +- .../src/desktop/get-open-window-args.ts | 8 +- packages/client-api/src/init.ts | 80 +++++++++++-------- packages/client-api/src/shims.d.ts | 2 +- 4 files changed, 51 insertions(+), 41 deletions(-) diff --git a/packages/client-api/src/desktop/desktop-commands.ts b/packages/client-api/src/desktop/desktop-commands.ts index 4b21da9b..d4693bd9 100644 --- a/packages/client-api/src/desktop/desktop-commands.ts +++ b/packages/client-api/src/desktop/desktop-commands.ts @@ -19,7 +19,7 @@ export function readConfigFile(): Promise { /** * Get args used to open the window with the {@link windowLabel}. */ -export function getOpenWindowArgs( +export function getInitialState( windowLabel: string, ): Promise { return invoke('get_open_window_args', { diff --git a/packages/client-api/src/desktop/get-open-window-args.ts b/packages/client-api/src/desktop/get-open-window-args.ts index 50945928..f3b3a2e7 100644 --- a/packages/client-api/src/desktop/get-open-window-args.ts +++ b/packages/client-api/src/desktop/get-open-window-args.ts @@ -2,7 +2,7 @@ import { getCurrentWindow } from '@tauri-apps/api/window'; import { createStore } from 'solid-js/store'; import type { OpenWindowArgs } from './shared'; -import { getOpenWindowArgs } from './desktop-commands'; +import { getInitialState } from './desktop-commands'; const [openWindowArgs, setOpenWindowArgs] = createStore({ value: null as OpenWindowArgs | null, @@ -15,9 +15,9 @@ export async function _getOpenWindowArgs() { } async function fetchOpenWindowArgs() { - if (window.__ZEBAR_OPEN_ARGS) { - return window.__ZEBAR_OPEN_ARGS; + if (window.__ZEBAR_INITIAL_STATE) { + return window.__ZEBAR_INITIAL_STATE; } - return getOpenWindowArgs(await getCurrentWindow().label); + return getInitialState(await getCurrentWindow().label); } diff --git a/packages/client-api/src/init.ts b/packages/client-api/src/init.ts index 442b7b85..6227bf1b 100644 --- a/packages/client-api/src/init.ts +++ b/packages/client-api/src/init.ts @@ -1,7 +1,7 @@ import { getCurrentWindow } from '@tauri-apps/api/window'; -import { getOwner } from 'solid-js'; +import { createRoot, getOwner, runWithOwner } from 'solid-js'; -import { getOpenWindowArgs, openWindow, showErrorDialog } from './desktop'; +import { getInitialState, openWindow, showErrorDialog } from './desktop'; import { createLogger } from '~/utils'; import type { ZebarContext } from './zebar-context.model'; @@ -18,38 +18,48 @@ export function init(callback: (context: ZebarContext) => void) { * Handles initialization. */ export async function initAsync(): Promise { - try { - // TODO: Create new root if owner is null. - const owner = getOwner()!; - - const windowState = - window.__ZEBAR_OPEN_ARGS ?? - (await getOpenWindowArgs(getCurrentWindow().label)); - - return { - config: windowState.config, - providers: windowState.providers, - openWindow, - // @ts-ignore - TODO - currentWindow: {}, - // @ts-ignore - TODO - allWindows: [], - // @ts-ignore - TODO - currentMonitor: {}, + return withReactiveContext(async () => { + try { + const currentWindow = getCurrentWindow(); + const initialState = + window.__ZEBAR_INITIAL_STATE ?? + (await getInitialState(currentWindow.label)); + + await currentWindow.show(); + // @ts-ignore - TODO - allMonitors: [], - }; - } catch (err) { - logger.error('Failed to initialize window:', err); - - await showErrorDialog({ - title: 'Failed to initialize window', - error: err, - }); - - // Error during window initialization is unrecoverable, so we close - // the window. - getCurrentWindow().close(); - throw err; - } + return { + config: initialState.config, + providers: initialState.providers, + openWindow, + currentWindow: {}, + allWindows: [], + currentMonitor: {}, + allMonitors: [], + } as ZebarContext; + } catch (err) { + logger.error('Failed to initialize window:', err); + + await showErrorDialog({ + title: 'Failed to initialize window', + error: err, + }); + + // Error during window initialization is unrecoverable, so we close + // the window. + getCurrentWindow().close(); + throw err; + } + }); +} + +/** + * Runs callback in a reactive context (allows for SolidJS reactivity). + */ +function withReactiveContext(callback: () => T) { + const owner = getOwner(); + + return owner + ? (runWithOwner(owner, callback) as T) + : createRoot(callback); } diff --git a/packages/client-api/src/shims.d.ts b/packages/client-api/src/shims.d.ts index 20848cf9..20969028 100644 --- a/packages/client-api/src/shims.d.ts +++ b/packages/client-api/src/shims.d.ts @@ -1,4 +1,4 @@ interface Window { __TAURI__: any; - __ZEBAR_OPEN_ARGS: import('./desktop').OpenWindowArgs; + __ZEBAR_INITIAL_STATE: import('./desktop').OpenWindowArgs; } From a876f43ab64b25b1144e41fa87280e3db61c7d21 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 22 Aug 2024 03:07:00 +0800 Subject: [PATCH 003/138] refactor: reorganize provider-related types --- .../client-api/src/desktop/shared/index.ts | 1 - .../battery}/battery-provider-config.model.ts | 0 .../cpu}/cpu-provider-config.model.ts | 0 .../src/providers/create-provider.ts | 12 +-- .../date}/date-provider-config.model.ts | 0 .../src/providers/get-element-providers.ts | 83 ------------------- .../glazewm}/glazewm-provider-config.model.ts | 0 .../host}/host-provider-config.model.ts | 0 packages/client-api/src/providers/index.ts | 2 - .../ip}/ip-provider-config.model.ts | 0 .../komorebi-provider-config.model.ts | 0 .../memory}/memory-provider-config.model.ts | 0 .../monitors-provider-config.model.ts | 0 .../network}/network-provider-config.model.ts | 0 .../provider-config.model.ts | 0 .../provider-type.model.ts | 1 - .../providers/self/create-self-provider.ts | 16 ---- .../util}/util-provider-config.model.ts | 0 .../weather}/weather-provider-config.model.ts | 0 .../src/user-config/window/index.ts | 3 - .../src/user-config/window/providers/index.ts | 13 --- .../providers/self-provider-config.model.ts | 9 -- 22 files changed, 2 insertions(+), 138 deletions(-) rename packages/client-api/src/{user-config/window/providers => providers/battery}/battery-provider-config.model.ts (100%) rename packages/client-api/src/{user-config/window/providers => providers/cpu}/cpu-provider-config.model.ts (100%) rename packages/client-api/src/{user-config/window/providers => providers/date}/date-provider-config.model.ts (100%) delete mode 100644 packages/client-api/src/providers/get-element-providers.ts rename packages/client-api/src/{user-config/window/providers => providers/glazewm}/glazewm-provider-config.model.ts (100%) rename packages/client-api/src/{user-config/window/providers => providers/host}/host-provider-config.model.ts (100%) rename packages/client-api/src/{user-config/window/providers => providers/ip}/ip-provider-config.model.ts (100%) rename packages/client-api/src/{user-config/window/providers => providers/komorebi}/komorebi-provider-config.model.ts (100%) rename packages/client-api/src/{user-config/window/providers => providers/memory}/memory-provider-config.model.ts (100%) rename packages/client-api/src/{user-config/window/providers => providers/monitors}/monitors-provider-config.model.ts (100%) rename packages/client-api/src/{user-config/window/providers => providers/network}/network-provider-config.model.ts (100%) rename packages/client-api/src/{user-config/window => providers}/provider-config.model.ts (100%) rename packages/client-api/src/{user-config/window => providers}/provider-type.model.ts (95%) delete mode 100644 packages/client-api/src/providers/self/create-self-provider.ts rename packages/client-api/src/{user-config/window/providers => providers/util}/util-provider-config.model.ts (100%) rename packages/client-api/src/{user-config/window/providers => providers/weather}/weather-provider-config.model.ts (100%) delete mode 100644 packages/client-api/src/user-config/window/index.ts delete mode 100644 packages/client-api/src/user-config/window/providers/index.ts delete mode 100644 packages/client-api/src/user-config/window/providers/self-provider-config.model.ts diff --git a/packages/client-api/src/desktop/shared/index.ts b/packages/client-api/src/desktop/shared/index.ts index dcea8b96..d9573444 100644 --- a/packages/client-api/src/desktop/shared/index.ts +++ b/packages/client-api/src/desktop/shared/index.ts @@ -1,3 +1,2 @@ export * from './monitor-info.model'; export * from './open-window-args.model'; -export * from './window-info.model'; diff --git a/packages/client-api/src/user-config/window/providers/battery-provider-config.model.ts b/packages/client-api/src/providers/battery/battery-provider-config.model.ts similarity index 100% rename from packages/client-api/src/user-config/window/providers/battery-provider-config.model.ts rename to packages/client-api/src/providers/battery/battery-provider-config.model.ts diff --git a/packages/client-api/src/user-config/window/providers/cpu-provider-config.model.ts b/packages/client-api/src/providers/cpu/cpu-provider-config.model.ts similarity index 100% rename from packages/client-api/src/user-config/window/providers/cpu-provider-config.model.ts rename to packages/client-api/src/providers/cpu/cpu-provider-config.model.ts diff --git a/packages/client-api/src/providers/create-provider.ts b/packages/client-api/src/providers/create-provider.ts index 5b6ec097..c0584289 100644 --- a/packages/client-api/src/providers/create-provider.ts +++ b/packages/client-api/src/providers/create-provider.ts @@ -10,18 +10,12 @@ import { createKomorebiProvider } from './komorebi/create-komorebi-provider'; import { createMemoryProvider } from './memory/create-memory-provider'; import { createMonitorsProvider } from './monitors/create-monitors-provider'; import { createNetworkProvider } from './network/create-network-provider'; -import { createSelfProvider } from './self/create-self-provider'; import { createUtilProvider } from './util/create-util-provider'; import { createWeatherProvider } from './weather/create-weather-provider'; -import { ProviderType, type ProviderConfig } from '~/user-config'; -import type { ElementContext } from '~/element-context.model'; -import type { PickPartial } from '~/utils'; +import type { ProviderConfig } from './provider-config.model'; +import { ProviderType } from './provider-type.model'; export async function createProvider( - elementContext: PickPartial< - ElementContext, - 'parsedConfig' | 'providers' - >, config: ProviderConfig, owner: Owner, ) { @@ -46,8 +40,6 @@ export async function createProvider( return createMonitorsProvider(config, owner); case ProviderType.NETWORK: return createNetworkProvider(config, owner); - case ProviderType.SELF: - return createSelfProvider(elementContext); case ProviderType.UTIL: return createUtilProvider(config, owner); case ProviderType.WEATHER: diff --git a/packages/client-api/src/user-config/window/providers/date-provider-config.model.ts b/packages/client-api/src/providers/date/date-provider-config.model.ts similarity index 100% rename from packages/client-api/src/user-config/window/providers/date-provider-config.model.ts rename to packages/client-api/src/providers/date/date-provider-config.model.ts diff --git a/packages/client-api/src/providers/get-element-providers.ts b/packages/client-api/src/providers/get-element-providers.ts deleted file mode 100644 index d06bedfe..00000000 --- a/packages/client-api/src/providers/get-element-providers.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { - type Accessor, - type Owner, - createComputed, - createSignal, - runWithOwner, -} from 'solid-js'; -import { createStore } from 'solid-js/store'; - -import { ProvidersConfigSchema, parseWithSchema } from '~/user-config'; -import type { ElementContext } from '~/element-context.model'; -import { createProvider } from './create-provider'; -import type { PickPartial } from '~/utils'; - -export async function getElementProviders( - elementContext: PickPartial< - ElementContext, - 'parsedConfig' | 'providers' - >, - ancestorProviders: Accessor>[], - owner: Owner, -) { - const [elementProviders, _] = createSignal(await getElementProviders()); - - const [mergedProviders, setMergedProviders] = createStore( - getMergedProviders(), - ); - - // Update the store on changes to any provider variables. - runWithOwner(owner, () => { - createComputed(() => setMergedProviders(getMergedProviders())); - }); - - /** - * Get map of element providers. - */ - async function getElementProviders() { - const providerConfigs = parseWithSchema( - ProvidersConfigSchema, - (elementContext.rawConfig as Record)?.providers ?? - [], - ); - - // Create tuple of configs and the created provider. - const providers = await Promise.all( - providerConfigs.map( - async config => - [ - config, - await createProvider(elementContext, config, owner), - ] as const, - ), - ); - - return providers.reduce( - (acc, [config, provider]) => ({ - ...acc, - [config.type]: provider, - }), - {}, - ); - } - - /** - * Get map of element providers merged with ancestor providers. - */ - function getMergedProviders() { - const mergedancestorProviders = (ancestorProviders ?? []).reduce( - (acc, vars) => ({ ...acc, ...vars() }), - {}, - ); - - return { - ...mergedancestorProviders, - ...elementProviders(), - }; - } - - return { - element: elementProviders, - merged: mergedProviders, - }; -} diff --git a/packages/client-api/src/user-config/window/providers/glazewm-provider-config.model.ts b/packages/client-api/src/providers/glazewm/glazewm-provider-config.model.ts similarity index 100% rename from packages/client-api/src/user-config/window/providers/glazewm-provider-config.model.ts rename to packages/client-api/src/providers/glazewm/glazewm-provider-config.model.ts diff --git a/packages/client-api/src/user-config/window/providers/host-provider-config.model.ts b/packages/client-api/src/providers/host/host-provider-config.model.ts similarity index 100% rename from packages/client-api/src/user-config/window/providers/host-provider-config.model.ts rename to packages/client-api/src/providers/host/host-provider-config.model.ts diff --git a/packages/client-api/src/providers/index.ts b/packages/client-api/src/providers/index.ts index d0106b1c..c51d6547 100644 --- a/packages/client-api/src/providers/index.ts +++ b/packages/client-api/src/providers/index.ts @@ -5,9 +5,7 @@ export * from './glazewm/create-glazewm-provider'; export * from './ip/create-ip-provider'; export * from './memory/create-memory-provider'; export * from './network/create-network-provider'; -export * from './self/create-self-provider'; export * from './util/create-util-provider'; export * from './weather/create-weather-provider'; export * from './create-provider-listener'; export * from './create-provider'; -export * from './get-element-providers'; diff --git a/packages/client-api/src/user-config/window/providers/ip-provider-config.model.ts b/packages/client-api/src/providers/ip/ip-provider-config.model.ts similarity index 100% rename from packages/client-api/src/user-config/window/providers/ip-provider-config.model.ts rename to packages/client-api/src/providers/ip/ip-provider-config.model.ts diff --git a/packages/client-api/src/user-config/window/providers/komorebi-provider-config.model.ts b/packages/client-api/src/providers/komorebi/komorebi-provider-config.model.ts similarity index 100% rename from packages/client-api/src/user-config/window/providers/komorebi-provider-config.model.ts rename to packages/client-api/src/providers/komorebi/komorebi-provider-config.model.ts diff --git a/packages/client-api/src/user-config/window/providers/memory-provider-config.model.ts b/packages/client-api/src/providers/memory/memory-provider-config.model.ts similarity index 100% rename from packages/client-api/src/user-config/window/providers/memory-provider-config.model.ts rename to packages/client-api/src/providers/memory/memory-provider-config.model.ts diff --git a/packages/client-api/src/user-config/window/providers/monitors-provider-config.model.ts b/packages/client-api/src/providers/monitors/monitors-provider-config.model.ts similarity index 100% rename from packages/client-api/src/user-config/window/providers/monitors-provider-config.model.ts rename to packages/client-api/src/providers/monitors/monitors-provider-config.model.ts diff --git a/packages/client-api/src/user-config/window/providers/network-provider-config.model.ts b/packages/client-api/src/providers/network/network-provider-config.model.ts similarity index 100% rename from packages/client-api/src/user-config/window/providers/network-provider-config.model.ts rename to packages/client-api/src/providers/network/network-provider-config.model.ts diff --git a/packages/client-api/src/user-config/window/provider-config.model.ts b/packages/client-api/src/providers/provider-config.model.ts similarity index 100% rename from packages/client-api/src/user-config/window/provider-config.model.ts rename to packages/client-api/src/providers/provider-config.model.ts diff --git a/packages/client-api/src/user-config/window/provider-type.model.ts b/packages/client-api/src/providers/provider-type.model.ts similarity index 95% rename from packages/client-api/src/user-config/window/provider-type.model.ts rename to packages/client-api/src/providers/provider-type.model.ts index 6ea9c221..75ec37a0 100644 --- a/packages/client-api/src/user-config/window/provider-type.model.ts +++ b/packages/client-api/src/providers/provider-type.model.ts @@ -11,7 +11,6 @@ export enum ProviderType { MEMORY = 'memory', MONITORS = 'monitors', NETWORK = 'network', - SELF = 'self', UTIL = 'util', WEATHER = 'weather', } diff --git a/packages/client-api/src/providers/self/create-self-provider.ts b/packages/client-api/src/providers/self/create-self-provider.ts deleted file mode 100644 index 21481b1c..00000000 --- a/packages/client-api/src/providers/self/create-self-provider.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { ElementContext } from '~/element-context.model'; -import type { PickPartial } from '~/utils'; - -export type SelfProvider = PickPartial< - ElementContext, - 'parsedConfig' | 'providers' ->; - -export async function createSelfProvider( - elementContext: PickPartial< - ElementContext, - 'parsedConfig' | 'providers' - >, -): Promise { - return elementContext; -} diff --git a/packages/client-api/src/user-config/window/providers/util-provider-config.model.ts b/packages/client-api/src/providers/util/util-provider-config.model.ts similarity index 100% rename from packages/client-api/src/user-config/window/providers/util-provider-config.model.ts rename to packages/client-api/src/providers/util/util-provider-config.model.ts diff --git a/packages/client-api/src/user-config/window/providers/weather-provider-config.model.ts b/packages/client-api/src/providers/weather/weather-provider-config.model.ts similarity index 100% rename from packages/client-api/src/user-config/window/providers/weather-provider-config.model.ts rename to packages/client-api/src/providers/weather/weather-provider-config.model.ts diff --git a/packages/client-api/src/user-config/window/index.ts b/packages/client-api/src/user-config/window/index.ts deleted file mode 100644 index 96c93c71..00000000 --- a/packages/client-api/src/user-config/window/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './provider-config.model'; -export * from './provider-type.model'; -export * from './providers'; diff --git a/packages/client-api/src/user-config/window/providers/index.ts b/packages/client-api/src/user-config/window/providers/index.ts deleted file mode 100644 index caca0198..00000000 --- a/packages/client-api/src/user-config/window/providers/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -export * from './battery-provider-config.model'; -export * from './cpu-provider-config.model'; -export * from './date-provider-config.model'; -export * from './glazewm-provider-config.model'; -export * from './host-provider-config.model'; -export * from './ip-provider-config.model'; -export * from './komorebi-provider-config.model'; -export * from './memory-provider-config.model'; -export * from './monitors-provider-config.model'; -export * from './network-provider-config.model'; -export * from './self-provider-config.model'; -export * from './util-provider-config.model'; -export * from './weather-provider-config.model'; diff --git a/packages/client-api/src/user-config/window/providers/self-provider-config.model.ts b/packages/client-api/src/user-config/window/providers/self-provider-config.model.ts deleted file mode 100644 index a0386720..00000000 --- a/packages/client-api/src/user-config/window/providers/self-provider-config.model.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { z } from 'zod'; - -import { ProviderType } from '../provider-type.model'; - -export const SelfProviderConfigSchema = z.object({ - type: z.literal(ProviderType.SELF), -}); - -export type SelfProviderConfig = z.infer; From 5d4f9ba8c224452442dab7a494bdead24b1c59ad Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 22 Aug 2024 17:21:01 +0800 Subject: [PATCH 004/138] feat: add new sample config --- .../src/desktop/desktop-commands.ts | 1 + .../src/desktop/get-open-window-args.ts | 8 +- packages/client-api/src/init.ts | 1 + .../client-api/src/zebar-context.model.ts | 9 + packages/desktop/resources/sample-config.yaml | 255 ++---------------- 5 files changed, 34 insertions(+), 240 deletions(-) diff --git a/packages/client-api/src/desktop/desktop-commands.ts b/packages/client-api/src/desktop/desktop-commands.ts index d4693bd9..9fb80cc1 100644 --- a/packages/client-api/src/desktop/desktop-commands.ts +++ b/packages/client-api/src/desktop/desktop-commands.ts @@ -76,6 +76,7 @@ export async function invoke( return response; } catch (err) { + logger.error(`Command '${command}' failed: ${err}`); throw new Error(`Command '${command}' failed: ${err}`); } } diff --git a/packages/client-api/src/desktop/get-open-window-args.ts b/packages/client-api/src/desktop/get-open-window-args.ts index f3b3a2e7..e00d79cd 100644 --- a/packages/client-api/src/desktop/get-open-window-args.ts +++ b/packages/client-api/src/desktop/get-open-window-args.ts @@ -1,13 +1,7 @@ import { getCurrentWindow } from '@tauri-apps/api/window'; -import { createStore } from 'solid-js/store'; -import type { OpenWindowArgs } from './shared'; import { getInitialState } from './desktop-commands'; -const [openWindowArgs, setOpenWindowArgs] = createStore({ - value: null as OpenWindowArgs | null, -}); - let promise: Promise | null = null; export async function _getOpenWindowArgs() { @@ -19,5 +13,5 @@ async function fetchOpenWindowArgs() { return window.__ZEBAR_INITIAL_STATE; } - return getInitialState(await getCurrentWindow().label); + return getInitialState(getCurrentWindow().label); } diff --git a/packages/client-api/src/init.ts b/packages/client-api/src/init.ts index 6227bf1b..d6372caf 100644 --- a/packages/client-api/src/init.ts +++ b/packages/client-api/src/init.ts @@ -32,6 +32,7 @@ export async function initAsync(): Promise { config: initialState.config, providers: initialState.providers, openWindow, + createProvider: () => {}, currentWindow: {}, allWindows: [], currentMonitor: {}, diff --git a/packages/client-api/src/zebar-context.model.ts b/packages/client-api/src/zebar-context.model.ts index 99a43cb0..a0c497e2 100644 --- a/packages/client-api/src/zebar-context.model.ts +++ b/packages/client-api/src/zebar-context.model.ts @@ -1,6 +1,7 @@ import { Window as TauriWindow } from '@tauri-apps/api/window'; import type { WindowConfig, WindowZOrder } from '~/user-config'; +import type { ProviderConfig } from './providers/provider-config.model'; export interface ZebarContext { /** @@ -28,6 +29,14 @@ export interface ZebarContext { configPath: string, args?: Record, ) => Promise; + + /** + * Initializes a provider. + * + * If an existing provider with the same config exists, that provider + * instance will be re-used. + */ + createProvider: (providerConfig: ProviderConfig) => Promise; } export interface ZebarWindow { diff --git a/packages/desktop/resources/sample-config.yaml b/packages/desktop/resources/sample-config.yaml index de225c57..15dcea13 100644 --- a/packages/desktop/resources/sample-config.yaml +++ b/packages/desktop/resources/sample-config.yaml @@ -1,233 +1,22 @@ -# Yaml is white-space sensitive (use 2 spaces to indent). - -### -# Define a new window with an id of 'bar'. This window can then be opened -# via the Zebar cli by running 'zebar open bar --args '. -# -# Docs regarding window: https://some-future-docs-link.com -window/bar: - providers: ['self'] - # Width of the window in physical pixels. - width: '{{ self.args.MONITOR_WIDTH }}' - # Height of the window in physical pixels. - height: '40' - # X-position of the window in physical pixels. - position_x: '{{ self.args.MONITOR_X }}' - # Y-position of the window in physical pixels. - position_y: '{{ self.args.MONITOR_Y }}' - # Whether to show the window above/below all others. - # Allowed values: 'always_on_top', 'always_on_bottom', 'normal'. - z_order: 'normal' - # Whether the window should be shown in the taskbar. - shown_in_taskbar: false - # Whether the window should have resize handles. - resizable: false - # Styles to apply globally within the window. For example, we can use - # this to import the Nerdfonts icon font. Ref https://www.nerdfonts.com/cheat-sheet - # for a cheatsheet of available Nerdfonts icons. - global_styles: | - @import "https://www.nerdfonts.com/assets/css/webfont.css"; - # CSS 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; - align-items: center; - height: 100%; - color: rgb(255 255 255 / 90%); - font-family: ui-monospace, monospace; - font-size: 12px; - padding: 4px 24px; - border-bottom: 1px solid rgb(255 255 255 / 5%);; - background: linear-gradient(rgb(0 0 0 / 90%), rgb(5 2 20 / 85%)); - - i { - color: rgb(115 130 175 / 95%); - margin-right: 7px; - } - - group/left: - styles: | - display: flex; - align-items: center; - - template/logo: - styles: | - margin-right: 20px; - template: | - - - template/glazewm_workspaces: - styles: | - display: flex; - align-items: center; - - .workspace { - background: rgb(255 255 255 / 5%); - margin-right: 4px; - padding: 4px 8px; - color: rgb(255 255 255 / 90%); - border: none; - border-radius: 2px; - cursor: pointer; - - &.displayed { - background: rgb(255 255 255 / 15%); - } - - &.focused, - &:hover { - background: rgb(75 115 255 / 50%); - } - } - providers: ['glazewm'] - events: - - type: 'click' - fn_path: 'script.js#focusWorkspace' - selector: '.workspace' - template: | - @for (workspace of glazewm.currentWorkspaces) { - - } - - group/center: - styles: | - justify-self: center; - - template/clock: - providers: ['date'] - # Available date tokens: https://moment.github.io/luxon/#/formatting?id=table-of-tokens - template: | - {{ date.toFormat(date.now, 'EEE d MMM t') }} - - group/right: - styles: | - justify-self: end; - display: flex; - align-items: center; - - .template { - margin-left: 20px; - } - - template/glazewm_other: - providers: ['glazewm'] - styles: | - .binding-mode, - .tiling-direction { - background: rgb(255 255 255 / 15%); - color: rgb(255 255 255 / 90%); - border-radius: 2px; - padding: 4px 6px; - margin: 0; - } - - .tiling-direction { - border: 0; - cursor: pointer; - } - - events: - - type: 'click' - fn_path: 'script.js#toggleTilingDirection' - selector: '.tiling-direction' - template: | - @for (bindingMode of glazewm.bindingModes) { - - {{ bindingMode.displayName ?? bindingMode.name }} - - } - - @if (glazewm.tilingDirection === 'horizontal') { - - } @else { - - } - - template/network: - providers: ['network'] - template: | - - @if (network.defaultInterface?.type === 'ethernet') { - - } @else if (network.defaultInterface?.type === 'wifi') { - @if (network.defaultGateway?.signalStrength >= 80) {} - @else if (network.defaultGateway?.signalStrength >= 65) {} - @else if (network.defaultGateway?.signalStrength >= 40) {} - @else if (network.defaultGateway?.signalStrength >= 25) {} - @else {} - {{ network.defaultGateway?.ssid }} - } @else { - - } - - template/memory: - providers: ['memory'] - template: | - - {{ Math.round(memory.usage) }}% - - template/cpu: - providers: ['cpu'] - styles: | - .high-usage { - color: #900029; - } - template: | - - - - @if (cpu.usage > 85) { - {{ Math.round(cpu.usage) }}% - } @else { - {{ Math.round(cpu.usage) }}% - } - - template/battery: - providers: ['battery'] - styles: | - position: relative; - - .charging-icon { - position: absolute; - font-size: 11px; - left: 7px; - top: 2px; - } - template: | - - @if (battery.isCharging) {} - - - @if (battery.chargePercent > 90) {} - @else if (battery.chargePercent > 70) {} - @else if (battery.chargePercent > 40) {} - @else if (battery.chargePercent > 20) {} - @else {} - - {{ Math.round(battery.chargePercent) }}% - - template/weather: - providers: ['weather'] - template: | - @switch (weather.status) { - @case ('clear_day') {} - @case ('clear_night') {} - @case ('cloudy_day') {} - @case ('cloudy_night') {} - @case ('light_rain_day') {} - @case ('light_rain_night') {} - @case ('heavy_rain_day') {} - @case ('heavy_rain_night') {} - @case ('snow_day') {} - @case ('snow_night') {} - @case ('thunder_day') {} - @case ('thunder_night') {} - } - {{ weather.celsiusTemp }}° +# Entry point HTML file. +html_path: './index.html' + +# Whether to show the window above/below all others. +# Allowed values: 'always_on_top', 'always_on_bottom', 'normal'. +z_order: 'normal' + +# Whether the window should be shown in the taskbar. +shown_in_taskbar: false + +# Whether the window should have resize handles. +resizable: false + +# Where to place the window. +default_placements: + # The monitor index to place the window on. + # Allowed values: 0, 1, 2, 3, etc. OR 'all'. + - monitor_index: 0 + # Allowed values: 'top', 'bottom', 'left', 'right'. + position: 'top' + offset_x: 20 + offset_y: 20 From 9fb2e0a9eaa0826475a0b27b4fe4d05321f5181a Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 22 Aug 2024 17:30:55 +0800 Subject: [PATCH 005/138] feat: improve sample config --- packages/desktop/resources/sample-config.yaml | 53 ++++++++++++------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/packages/desktop/resources/sample-config.yaml b/packages/desktop/resources/sample-config.yaml index 15dcea13..4b2a9d1a 100644 --- a/packages/desktop/resources/sample-config.yaml +++ b/packages/desktop/resources/sample-config.yaml @@ -1,22 +1,37 @@ # Entry point HTML file. html_path: './index.html' -# Whether to show the window above/below all others. -# Allowed values: 'always_on_top', 'always_on_bottom', 'normal'. -z_order: 'normal' - -# Whether the window should be shown in the taskbar. -shown_in_taskbar: false - -# Whether the window should have resize handles. -resizable: false - -# Where to place the window. -default_placements: - # The monitor index to place the window on. - # Allowed values: 0, 1, 2, 3, etc. OR 'all'. - - monitor_index: 0 - # Allowed values: 'top', 'bottom', 'left', 'right'. - position: 'top' - offset_x: 20 - offset_y: 20 +# Default options for when the window is opened. +# These can be overridden when opening the window programmatically. +launch_options: + # Whether to show the window above/below all others. + # Allowed values: 'always_on_top', 'always_on_bottom', 'normal'. + z_order: 'normal' + + # Whether the window should be shown in the taskbar. + shown_in_taskbar: false + + # Whether the window should have resize handles. + resizable: false + + # Where to place the window. + placement: + # The monitor index to place the window on. + # Allowed values: 0, 1, 2, 3, etc. or 'all'. + - monitor: 0 + # Anchor-point of the window. + # Allowed values: either 'center' or combinations of 'top', 'center', + # 'bottom' and 'left', 'center', 'right'. + anchor: 'top left' + + # Offset from the anchor-point. + offset_x: 20 + + # Offset from the anchor-point. + offset_y: 20 + + # Width of the window in % or physical pixels. + width: '100%' + + # Height of the window in % or physical pixels. + height: '30px' From 6695a30d100f2fea0b204cdbb73385411612bf5d Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 22 Aug 2024 18:24:43 +0800 Subject: [PATCH 006/138] feat: remove start script --- packages/desktop/package.json | 3 +- packages/desktop/resources/draft.yaml | 48 ------------------- packages/desktop/resources/sample-config.yaml | 4 ++ packages/desktop/resources/script.js | 10 ---- packages/desktop/resources/start.bat | 5 -- packages/desktop/resources/start.sh | 3 -- 6 files changed, 5 insertions(+), 68 deletions(-) delete mode 100644 packages/desktop/resources/draft.yaml delete mode 100644 packages/desktop/resources/script.js delete mode 100644 packages/desktop/resources/start.bat delete mode 100644 packages/desktop/resources/start.sh diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 7ca6f3dd..67a7a596 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -3,10 +3,9 @@ "version": "0.0.0", "scripts": { "build": "npm run tauri build -- --verbose", - "dev": "npm run -s monitors -- --print0 | xargs -0 -P 99 -I % sh -c 'npm run tauri dev -- -- -- open bar --args %'", + "dev": "npm run tauri dev", "format": "cargo fmt", "lint": "cargo fmt --check", - "monitors": "cargo run --no-default-features --quiet -- monitors", "tauri": "tauri" }, "dependencies": {}, diff --git a/packages/desktop/resources/draft.yaml b/packages/desktop/resources/draft.yaml deleted file mode 100644 index e60b5b57..00000000 --- a/packages/desktop/resources/draft.yaml +++ /dev/null @@ -1,48 +0,0 @@ -# Allowed values: 'window', 'component' -type: 'window' -# Whether to show the window above/below all others. -# Allowed values: 'always_on_top', 'always_on_bottom', 'normal'. -z_order: 'normal' -# Whether the window should be shown in the taskbar. -shown_in_taskbar: false -# Whether the window should have resize handles. -resizable: false -html_url: 'index.html' -# Where to place the window. -positions: - # The monitor index to place the window on. - # Allowed values: 0, 1, 2, 3, etc. OR 'all'. - - monitor_index: 0 - # Allowed values: 'top', 'bottom', 'left', 'right'. - position: 'top' - offset_x: 20 - offset_y: 20 -inputs: - - type: 'number' - name: 'CPU high threshold' - value: 50 - - type: 'color' - name: 'background' - value: '#fff' - - type: 'component' - name: 'clock_component' - - type: 'component_list' - name: 'left_group' - value: - [ - { type: 'component', name: 'logo_component' }, - { type: 'component', name: 'glazewm_workspaces_component' }, - ] -# # Problems with current: -# # * start script (ideally someone just launches Zebar from start menu). -# # * reacting to monitor removals/additions. -# # * improvements to rendering. -# # * nested structure kind of sucks. a template file would be better (e.g. a `zhtml` file). -# # * however, a template file means re-rendering the entire template string. -# # * the more optimizations that we make, the closer we get to essentially creating a frontend framework. - -### Needed for MVP: -# 1. Config above without component support (including monitor positioning). -# 2. No UI. -# 3. Load windows via system tray. -# 4. Traverse yaml files in `~/.glzr/zebar` diff --git a/packages/desktop/resources/sample-config.yaml b/packages/desktop/resources/sample-config.yaml index 4b2a9d1a..fee5430c 100644 --- a/packages/desktop/resources/sample-config.yaml +++ b/packages/desktop/resources/sample-config.yaml @@ -14,11 +14,15 @@ launch_options: # Whether the window should have resize handles. resizable: false + # Whether the window frame should be transparent. + transparent: true + # Where to place the window. placement: # The monitor index to place the window on. # Allowed values: 0, 1, 2, 3, etc. or 'all'. - monitor: 0 + # Anchor-point of the window. # Allowed values: either 'center' or combinations of 'top', 'center', # 'bottom' and 'left', 'center', 'right'. diff --git a/packages/desktop/resources/script.js b/packages/desktop/resources/script.js deleted file mode 100644 index 3af809c0..00000000 --- a/packages/desktop/resources/script.js +++ /dev/null @@ -1,10 +0,0 @@ -export function focusWorkspace(event, context) { - console.log('Focus button clicked!', event, context); - const id = event.target.id; - context.providers.glazewm.focusWorkspace(id); -} - -export function toggleTilingDirection(event, context) { - console.log('Tiling direction toggled!', event, context); - context.providers.glazewm.toggleTilingDirection(); -} diff --git a/packages/desktop/resources/start.bat b/packages/desktop/resources/start.bat deleted file mode 100644 index 64bf55eb..00000000 --- a/packages/desktop/resources/start.bat +++ /dev/null @@ -1,5 +0,0 @@ -@echo off -@REM Start hidden powershell script, which runs `zebar open bar --args ...` for every monitor. -powershell -WindowStyle hidden -Command ^ - $monitors = zebar monitors; ^ - foreach ($monitor in $monitors) { Start-Process -WindowStyle Hidden -FilePath \"zebar\" -ArgumentList \"open bar --args $monitor\" }; diff --git a/packages/desktop/resources/start.sh b/packages/desktop/resources/start.sh deleted file mode 100644 index ec6e6642..00000000 --- a/packages/desktop/resources/start.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash -# Run `zebar open bar --args ...` for every monitor. -zebar monitors --print0 | xargs -0 -P 99 -I % sh -c 'zebar open bar --args %' From 2ad9a1ea687e4f503aeba861761233a9bb5aa417 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 22 Aug 2024 19:07:30 +0800 Subject: [PATCH 007/138] feat: organize entry point depending on cli subcommand --- packages/desktop/src/cli.rs | 21 +++-- packages/desktop/src/main.rs | 116 ++++++++++++++----------- packages/desktop/src/window_factory.rs | 8 +- 3 files changed, 79 insertions(+), 66 deletions(-) diff --git a/packages/desktop/src/cli.rs b/packages/desktop/src/cli.rs index f63ba554..7038e55a 100644 --- a/packages/desktop/src/cli.rs +++ b/packages/desktop/src/cli.rs @@ -13,22 +13,25 @@ pub struct Cli { #[derive(Subcommand, Debug)] pub enum CliCommand { - /// Open a window by its ID (eg. `zebar open bar`). + /// Open a window by its relative file path within the Zebar config + /// directory (e.g. `zebar open ./material/config.yaml`). + /// + /// Starts Zebar if it is not already running. Open(OpenWindowArgs), + + /// Open all default windows. + /// + /// Starts Zebar if it is not already running. + OpenAll, + /// Output available monitors. Monitors(OutputMonitorsArgs), } #[derive(Args, Debug)] pub struct OpenWindowArgs { - /// ID of the window to open (eg. `bar`). - pub window_id: String, - - /// Arguments to pass to the window. - /// - /// These become available via the `self` provider. - #[clap(short, long, num_args = 1.., value_parser=parse_open_args)] - pub args: Option>, + /// Relative file path within the Zebar config directory. + pub config_path: String, } #[derive(Args, Debug)] diff --git a/packages/desktop/src/main.rs b/packages/desktop/src/main.rs index b336dfb5..913fcf2b 100644 --- a/packages/desktop/src/main.rs +++ b/packages/desktop/src/main.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, env}; use clap::Parser; -use cli::OpenWindowArgs; +use cli::{OpenWindowArgs, OutputMonitorsArgs}; use providers::config::ProviderConfig; use tauri::{AppHandle, Manager, State, Window}; use tracing::level_filters::LevelFilter; @@ -44,12 +44,12 @@ async fn get_open_window_args( #[tauri::command] fn open_window( - window_id: String, + config_path: String, args: HashMap, window_factory: State<'_, WindowFactory>, ) -> anyhow::Result<(), String> { window_factory.try_open(OpenWindowArgs { - window_id, + config_path, args: Some(args.into_iter().collect()), }); @@ -111,8 +111,31 @@ fn set_skip_taskbar( Ok(()) } +/// Main entry point for the application. +/// +/// Conditionally starts Zebar or runs a CLI command based on the given +/// subcommand. #[tokio::main] async fn main() { + let cli = Cli::parse(); + + match cli.command { + CliCommand::Monitors(args) => output_monitors(args), + _ => start_app(cli), + } +} + +/// Prints available monitors to console. +fn output_monitors(args: OutputMonitorsArgs) { + tauri::Builder::default().setup(|app| { + let monitors_str = get_monitors_str(app, args); + cli::print_and_exit(monitors_str); + Ok(()) + }); +} + +/// Starts Zebar - either with a specific window or all windows. +fn start_app(cli: Cli) { tracing_subscriber::fmt() .with_env_filter( EnvFilter::from_env("LOG_LEVEL") @@ -124,56 +147,43 @@ async fn main() { tauri::Builder::default() .setup(|app| { - let cli = Cli::parse(); - - // Since most Tauri plugins and setup is not needed for the - // `monitors` CLI command, the setup is conditional based on - // the CLI command. - match cli.command { - CliCommand::Monitors(args) => { - let monitors_str = get_monitors_str(app, args); - cli::print_and_exit(monitors_str); - Ok(()) - } - CliCommand::Open(open_args) => { - // If this is not the first instance of the app, this will emit - // within the original instance and exit immediately. - app.handle().plugin(tauri_plugin_single_instance::init( - move |app, args, _| { - let cli = Cli::parse_from(args); - - // CLI command is guaranteed to be an open command here. - if let CliCommand::Open(args) = cli.command { - app.state::().try_open(args); - } - }, - ))?; - - // Prevent windows from showing up in the dock on MacOS. - #[cfg(target_os = "macos")] - app.set_activation_policy(tauri::ActivationPolicy::Accessory); - - // Open window with the given args and initialize `WindowFactory` - // in Tauri state. - let window_factory = WindowFactory::new(app.handle()); - window_factory.try_open(open_args); - app.manage(window_factory); - - app.handle().plugin(tauri_plugin_shell::init())?; - app.handle().plugin(tauri_plugin_http::init())?; - app.handle().plugin(tauri_plugin_dialog::init())?; - - // Initialize `ProviderManager` in Tauri state. - let mut manager = ProviderManager::new(); - manager.init(app.handle()); - app.manage(manager); - - // Add application icon to system tray. - setup_sys_tray(app)?; - - Ok(()) - } - } + // If this is not the first instance of the app, this will + // emit within the original instance and exit + // immediately. + app.handle().plugin(tauri_plugin_single_instance::init( + move |app, args, _| { + let cli = Cli::parse_from(args); + + // CLI command is guaranteed to be an open command here. + if let CliCommand::Open(args) = cli.command { + app.state::().try_open(args); + } + }, + ))?; + + // Prevent windows from showing up in the dock on MacOS. + #[cfg(target_os = "macos")] + app.set_activation_policy(tauri::ActivationPolicy::Accessory); + + // Open window with the given args and initialize + // `WindowFactory` in Tauri state. + let window_factory = WindowFactory::new(app.handle()); + window_factory.try_open(open_args); + app.manage(window_factory); + + app.handle().plugin(tauri_plugin_shell::init())?; + app.handle().plugin(tauri_plugin_http::init())?; + app.handle().plugin(tauri_plugin_dialog::init())?; + + // Initialize `ProviderManager` in Tauri state. + let mut manager = ProviderManager::new(); + manager.init(app.handle()); + app.manage(manager); + + // Add application icon to system tray. + setup_sys_tray(app)?; + + Ok(()) }) .invoke_handler(tauri::generate_handler![ read_config_file, diff --git a/packages/desktop/src/window_factory.rs b/packages/desktop/src/window_factory.rs index a32e5524..93c8a0b7 100644 --- a/packages/desktop/src/window_factory.rs +++ b/packages/desktop/src/window_factory.rs @@ -71,21 +71,21 @@ impl WindowFactory { info!( "Creating window #{} '{}' with args: {:#?}", - window_count, open_args.window_id, args + window_count, open_args.config_path, args ); // Window label needs to be globally unique. Hence add a prefix with // the window count to handle cases where multiple of the same window // are opened. let window_label = - format!("{}-{}", window_count, &open_args.window_id); + format!("{}-{}", window_count, &open_args.config_path); let window = WebviewWindowBuilder::new( app_handle, &window_label, WebviewUrl::default(), ) - .title(format!("Zebar - {}", open_args.window_id)) + .title(format!("Zebar - {}", open_args.config_path)) .inner_size(500., 500.) .focused(false) .skip_taskbar(true) @@ -97,7 +97,7 @@ impl WindowFactory { .build()?; let state = WindowState { - window_id: open_args.window_id.clone(), + window_id: open_args.config_path.clone(), window_label: window_label.clone(), args, env: std::env::vars().collect(), From 8d26110fd9e8a4366644f4d2167d4e8964b1edf4 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 22 Aug 2024 19:08:00 +0800 Subject: [PATCH 008/138] feat: add window config serialization --- Cargo.lock | 1 + packages/desktop/Cargo.toml | 1 + packages/desktop/src/cli.rs | 6 ++ packages/desktop/src/main.rs | 24 ++--- packages/desktop/src/user_config.rs | 85 ++++++++++++++++- packages/desktop/src/util/length_value.rs | 106 ++++++++++++++++++++++ packages/desktop/src/util/mod.rs | 6 +- 7 files changed, 208 insertions(+), 21 deletions(-) create mode 100644 packages/desktop/src/util/length_value.rs diff --git a/Cargo.lock b/Cargo.lock index 7e0ae722..83b07e7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6514,6 +6514,7 @@ dependencies = [ "reqwest 0.11.27", "serde", "serde_json", + "serde_yaml", "starship-battery", "sysinfo", "tauri", diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 668c3ab9..6f57deb1 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -29,6 +29,7 @@ tauri-plugin-shell = "2.0.0-beta" tauri-plugin-single-instance = "2.0.0-beta" serde = { version = "1", features = ["derive"] } serde_json = "1" +serde_yaml = "0.9" starship-battery = "0.8" sysinfo = "0.30" tokio = { version = "1.33", features = ["full"] } diff --git a/packages/desktop/src/cli.rs b/packages/desktop/src/cli.rs index 7038e55a..02027e7d 100644 --- a/packages/desktop/src/cli.rs +++ b/packages/desktop/src/cli.rs @@ -32,6 +32,12 @@ pub enum CliCommand { pub struct OpenWindowArgs { /// Relative file path within the Zebar config directory. pub config_path: String, + + /// Arguments to pass to the window. + /// + /// These become available via the `self` provider. + #[clap(short, long, num_args = 1.., value_parser=parse_open_args)] + pub args: Option>, } #[derive(Args, Debug)] diff --git a/packages/desktop/src/main.rs b/packages/desktop/src/main.rs index 913fcf2b..b9328add 100644 --- a/packages/desktop/src/main.rs +++ b/packages/desktop/src/main.rs @@ -2,19 +2,17 @@ use std::{collections::HashMap, env}; use clap::Parser; -use cli::{OpenWindowArgs, OutputMonitorsArgs}; -use providers::config::ProviderConfig; -use tauri::{AppHandle, Manager, State, Window}; +use tauri::{Manager, State, Window}; use tracing::level_filters::LevelFilter; use tracing_subscriber::EnvFilter; -use window_factory::{WindowFactory, WindowState}; use crate::{ - cli::{Cli, CliCommand}, + cli::{Cli, CliCommand, OpenWindowArgs, OutputMonitorsArgs}, monitors::get_monitors_str, - providers::provider_manager::ProviderManager, + providers::{config::ProviderConfig, provider_manager::ProviderManager}, sys_tray::setup_sys_tray, - util::window_ext::WindowExt, + util::WindowExt, + window_factory::{WindowFactory, WindowState}, }; mod cli; @@ -25,15 +23,6 @@ mod user_config; mod util; mod window_factory; -#[tauri::command] -fn read_config_file( - config_path_override: Option<&str>, - app_handle: AppHandle, -) -> anyhow::Result { - user_config::read_file(config_path_override, app_handle) - .map_err(|err| err.to_string()) -} - #[tauri::command] async fn get_open_window_args( window_label: String, @@ -161,6 +150,8 @@ fn start_app(cli: Cli) { }, ))?; + let config = user_config::read_file(None, app.handle())?; + // Prevent windows from showing up in the dock on MacOS. #[cfg(target_os = "macos")] app.set_activation_policy(tauri::ActivationPolicy::Accessory); @@ -186,7 +177,6 @@ fn start_app(cli: Cli) { Ok(()) }) .invoke_handler(tauri::generate_handler![ - read_config_file, get_open_window_args, open_window, listen_provider, diff --git a/packages/desktop/src/user_config.rs b/packages/desktop/src/user_config.rs index 652ac23b..9ca19f72 100644 --- a/packages/desktop/src/user_config.rs +++ b/packages/desktop/src/user_config.rs @@ -1,13 +1,85 @@ use std::{fs, path::PathBuf}; use anyhow::Context; +use serde::{Deserialize, Serialize}; use tauri::{path::BaseDirectory, AppHandle, Manager}; +use crate::util::LengthValue; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct WindowConfig { + /// Entry point HTML file. + html_path: String, + + /// Default options for when the window is opened. + launch_options: WindowLaunchOptions, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct WindowLaunchOptions { + /// Whether to show the window above/below all others. + z_order: WindowZOrder, + + /// Whether the window should be shown in the taskbar. + shown_in_taskbar: bool, + + /// Whether the window should have resize handles. + resizable: bool, + + /// Whether the window frame should be transparent. + transparent: bool, + + /// Where to place the window. + placement: WindowPlacement, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum WindowZOrder { + AlwaysOnBottom, + AlwaysOnTop, + Normal, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct WindowPlacement { + /// The monitor index to place the window on. + monitor: u32, + + /// Anchor-point of the window. + anchor: WindowAnchor, + + /// Offset from the anchor-point. + offset_x: LengthValue, + + /// Offset from the anchor-point. + offset_y: LengthValue, + + /// Width of the window in % or physical pixels. + width: LengthValue, + + /// Height of the window in % or physical pixels. + height: LengthValue, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum WindowAnchor { + TopLeft, + TopCenter, + TopRight, + CenterLeft, + Center, + CenterCenter, + CenterRight, + BottomLeft, + BottomCenter, + BottomRight, +} + /// Reads the config file at `~/.glzr/zebar/config.yaml`. pub fn read_file( config_path_override: Option<&str>, - app_handle: AppHandle, -) -> anyhow::Result { + app_handle: &AppHandle, +) -> anyhow::Result { let default_config_path = app_handle .path() .resolve(".glzr/zebar/config.yaml", BaseDirectory::Home) @@ -23,7 +95,14 @@ pub fn read_file( create_from_sample(&config_path, app_handle)?; } - fs::read_to_string(&config_path).context("Unable to read config file.") + let config_str = fs::read_to_string(&config_path) + .context("Unable to read config file.")?; + + // TODO: Improve error formatting of serde_yaml errors. Something + // similar to https://github.com/AlexanderThaller/format_serde_error + let config_value = serde_yaml::from_str(&config_str)?; + + Ok(config_value) } /// Initialize config at the given path from the sample config resource. diff --git a/packages/desktop/src/util/length_value.rs b/packages/desktop/src/util/length_value.rs new file mode 100644 index 00000000..e67bd5a8 --- /dev/null +++ b/packages/desktop/src/util/length_value.rs @@ -0,0 +1,106 @@ +use std::str::FromStr; + +use anyhow::{bail, Context}; +use regex::Regex; +use serde::{Deserialize, Deserializer, Serialize}; + +#[derive(Debug, Clone, PartialEq, Serialize)] +pub struct LengthValue { + pub amount: f32, + pub unit: LengthUnit, +} + +#[derive(Debug, Deserialize, Clone, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum LengthUnit { + Percentage, + Pixel, +} + +impl LengthValue { + pub fn from_px(px: i32) -> Self { + Self { + amount: px as f32, + unit: LengthUnit::Pixel, + } + } + + pub fn to_px(&self, total_px: i32) -> i32 { + match self.unit { + LengthUnit::Percentage => (self.amount * total_px as f32) as i32, + LengthUnit::Pixel => self.amount as i32, + } + } + + pub fn to_percentage(&self, total_px: i32) -> f32 { + match self.unit { + LengthUnit::Percentage => self.amount, + LengthUnit::Pixel => self.amount / total_px as f32, + } + } +} + +impl FromStr for LengthValue { + type Err = anyhow::Error; + + /// Parses a string containing a number followed by a unit (`px`, `%`). + /// Allows for negative numbers. + /// + /// Example: + /// ``` + /// LengthValue::from_str("100px") // { amount: 100.0, unit: LengthUnit::Pixel } + /// ``` + fn from_str(unparsed: &str) -> anyhow::Result { + let units_regex = Regex::new(r"([+-]?\d+)(%|px)?")?; + + let err_msg = format!( + "Not a valid length value '{}'. Must be of format '10px' or '10%'.", + unparsed + ); + + let captures = units_regex + .captures(unparsed) + .context(err_msg.to_string())?; + + let unit_str = captures.get(2).map_or("", |m| m.as_str()); + let unit = match unit_str { + "px" | "" => LengthUnit::Pixel, + "%" => LengthUnit::Percentage, + _ => bail!(err_msg), + }; + + let amount = captures + .get(1) + .and_then(|amount_str| f32::from_str(amount_str.into()).ok()) + // Store percentage units as a fraction of 1. + .map(|amount| match unit { + LengthUnit::Pixel => amount, + LengthUnit::Percentage => amount / 100.0, + }) + .context(err_msg.to_string())?; + + Ok(LengthValue { amount, unit }) + } +} + +/// Deserialize a `LengthValue` from either a string or a struct. +impl<'de> Deserialize<'de> for LengthValue { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(untagged)] + enum LengthValueDe { + Struct { amount: f32, unit: LengthUnit }, + String(String), + } + + match LengthValueDe::deserialize(deserializer)? { + LengthValueDe::Struct { amount, unit } => Ok(Self { amount, unit }), + LengthValueDe::String(str) => { + Self::from_str(&str).map_err(serde::de::Error::custom) + } + } + } +} diff --git a/packages/desktop/src/util/mod.rs b/packages/desktop/src/util/mod.rs index 7ccdb6b0..000b4f60 100644 --- a/packages/desktop/src/util/mod.rs +++ b/packages/desktop/src/util/mod.rs @@ -1 +1,5 @@ -pub mod window_ext; +mod length_value; +mod window_ext; + +pub use length_value::*; +pub use window_ext::*; From 0f71642a3bd42b121e166df466e6edd73e440294 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 22 Aug 2024 19:45:28 +0800 Subject: [PATCH 009/138] feat: get rust to compile --- packages/desktop/package.json | 2 +- packages/desktop/resources/sample-config.yaml | 4 +-- packages/desktop/src/main.rs | 10 +++--- packages/desktop/src/user_config.rs | 6 ++-- packages/desktop/src/window_factory.rs | 36 +++++++------------ 5 files changed, 24 insertions(+), 34 deletions(-) diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 67a7a596..794d8e06 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "scripts": { "build": "npm run tauri build -- --verbose", - "dev": "npm run tauri dev", + "dev": "npm run tauri dev -- -- -- open-all", "format": "cargo fmt", "lint": "cargo fmt --check", "tauri": "tauri" diff --git a/packages/desktop/resources/sample-config.yaml b/packages/desktop/resources/sample-config.yaml index fee5430c..4421fa0b 100644 --- a/packages/desktop/resources/sample-config.yaml +++ b/packages/desktop/resources/sample-config.yaml @@ -29,10 +29,10 @@ launch_options: anchor: 'top left' # Offset from the anchor-point. - offset_x: 20 + offset_x: '20px' # Offset from the anchor-point. - offset_y: 20 + offset_y: '20px' # Width of the window in % or physical pixels. width: '100%' diff --git a/packages/desktop/src/main.rs b/packages/desktop/src/main.rs index b9328add..2e235697 100644 --- a/packages/desktop/src/main.rs +++ b/packages/desktop/src/main.rs @@ -37,10 +37,8 @@ fn open_window( args: HashMap, window_factory: State<'_, WindowFactory>, ) -> anyhow::Result<(), String> { - window_factory.try_open(OpenWindowArgs { - config_path, - args: Some(args.into_iter().collect()), - }); + // TODO: Pass in `WindowConfig`. + window_factory.try_open(); Ok(()) } @@ -145,7 +143,7 @@ fn start_app(cli: Cli) { // CLI command is guaranteed to be an open command here. if let CliCommand::Open(args) = cli.command { - app.state::().try_open(args); + app.state::().try_open(); } }, ))?; @@ -159,7 +157,7 @@ fn start_app(cli: Cli) { // Open window with the given args and initialize // `WindowFactory` in Tauri state. let window_factory = WindowFactory::new(app.handle()); - window_factory.try_open(open_args); + window_factory.try_open(); app.manage(window_factory); app.handle().plugin(tauri_plugin_shell::init())?; diff --git a/packages/desktop/src/user_config.rs b/packages/desktop/src/user_config.rs index 9ca19f72..f2b3d2d0 100644 --- a/packages/desktop/src/user_config.rs +++ b/packages/desktop/src/user_config.rs @@ -30,10 +30,11 @@ pub struct WindowLaunchOptions { transparent: bool, /// Where to place the window. - placement: WindowPlacement, + placement: Vec, } #[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] pub enum WindowZOrder { AlwaysOnBottom, AlwaysOnTop, @@ -62,6 +63,7 @@ pub struct WindowPlacement { } #[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] pub enum WindowAnchor { TopLeft, TopCenter, @@ -108,7 +110,7 @@ pub fn read_file( /// Initialize config at the given path from the sample config resource. fn create_from_sample( config_path: &PathBuf, - app_handle: AppHandle, + app_handle: &AppHandle, ) -> anyhow::Result<()> { let sample_path = app_handle .path() diff --git a/packages/desktop/src/window_factory.rs b/packages/desktop/src/window_factory.rs index 93c8a0b7..f5db9bcf 100644 --- a/packages/desktop/src/window_factory.rs +++ b/packages/desktop/src/window_factory.rs @@ -11,7 +11,7 @@ use tauri::{AppHandle, WebviewUrl, WebviewWindowBuilder}; use tokio::{sync::Mutex, task}; use tracing::{error, info}; -use crate::{cli::OpenWindowArgs, util::window_ext::WindowExt}; +use crate::util::WindowExt; /// Manages the creation of Zebar windows. pub struct WindowFactory { @@ -38,7 +38,8 @@ impl WindowFactory { } } - pub fn try_open(&self, open_args: OpenWindowArgs) { + /// TODO: Pass in `WindowConfig`. + pub fn try_open(&self) { let app_handle = self.app_handle.clone(); let window_states = self.window_states.clone(); let app_handle = app_handle.clone(); @@ -48,7 +49,7 @@ impl WindowFactory { // Increment number of windows. let new_count = window_count.fetch_add(1, Ordering::Relaxed) + 1; - let open_res = Self::open(&app_handle, open_args, new_count); + let open_res = Self::open(&app_handle, new_count); match open_res { Ok(state) => { @@ -64,47 +65,36 @@ impl WindowFactory { fn open( app_handle: &AppHandle, - open_args: OpenWindowArgs, window_count: u32, ) -> anyhow::Result { - let args = open_args.args.unwrap_or(vec![]).into_iter().collect(); - - info!( - "Creating window #{} '{}' with args: {:#?}", - window_count, open_args.config_path, args - ); - - // Window label needs to be globally unique. Hence add a prefix with - // the window count to handle cases where multiple of the same window - // are opened. - let window_label = - format!("{}-{}", window_count, &open_args.config_path); + info!("Creating window #{}", window_count); let window = WebviewWindowBuilder::new( app_handle, - &window_label, + // Window label needs to be globally unique. + window_count.to_string(), WebviewUrl::default(), ) - .title(format!("Zebar - {}", open_args.config_path)) + .title("Zebar") .inner_size(500., 500.) .focused(false) .skip_taskbar(true) .visible_on_all_workspaces(true) - .transparent(true) + .transparent(false) .shadow(false) .decorations(false) .resizable(false) .build()?; let state = WindowState { - window_id: open_args.config_path.clone(), - window_label: window_label.clone(), - args, + window_id: window_count.to_string(), + window_label: window_count.to_string(), + args: HashMap::new(), env: std::env::vars().collect(), }; _ = window.eval(&format!( - "window.__ZEBAR_OPEN_ARGS={}", + "window.__ZEBAR_INITIAL_STATE={}", serde_json::to_string(&state)? )); From 0bc3f20e40c5faef00d2c17844bd22b62b39199d Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 22 Aug 2024 19:45:59 +0800 Subject: [PATCH 010/138] feat: add provider configs alongside provider logic in client-api --- packages/client-api/src/init.ts | 2 + .../battery/battery-provider-config.model.ts | 13 ---- .../battery/create-battery-provider.ts | 8 ++- .../cpu/cpu-provider-config.model.ts | 11 ---- .../src/providers/cpu/create-cpu-provider.ts | 8 ++- .../src/providers/create-provider-listener.ts | 2 +- .../src/providers/create-provider.ts | 3 - .../providers/date/create-date-provider.ts | 25 +++++++- .../date/date-provider-config.model.ts | 28 --------- .../glazewm/create-glazewm-provider.ts | 8 ++- .../glazewm/glazewm-provider-config.model.ts | 11 ---- .../providers/host/create-host-provider.ts | 8 ++- .../host/host-provider-config.model.ts | 11 ---- .../src/providers/ip/create-ip-provider.ts | 8 ++- .../providers/ip/ip-provider-config.model.ts | 11 ---- .../komorebi/create-komorebi-provider.ts | 6 +- .../komorebi-provider-config.model.ts | 11 ---- .../memory/create-memory-provider.ts | 8 ++- .../memory/memory-provider-config.model.ts | 13 ---- .../monitors/create-monitors-provider.ts | 30 ---------- .../monitors-provider-config.model.ts | 11 ---- .../network/create-network-provider.ts | 8 ++- .../network/network-provider-config.model.ts | 13 ---- .../src/providers/provider-config.model.ts | 60 +++++++------------ .../src/providers/provider-type.model.ts | 1 - .../providers/util/create-util-provider.ts | 6 +- .../util/util-provider-config.model.ts | 9 --- .../weather/create-weather-provider.ts | 23 ++++++- .../weather/weather-provider-config.model.ts | 28 --------- packages/client/index.html | 28 +++++---- 30 files changed, 146 insertions(+), 266 deletions(-) delete mode 100644 packages/client-api/src/providers/battery/battery-provider-config.model.ts delete mode 100644 packages/client-api/src/providers/cpu/cpu-provider-config.model.ts delete mode 100644 packages/client-api/src/providers/date/date-provider-config.model.ts delete mode 100644 packages/client-api/src/providers/glazewm/glazewm-provider-config.model.ts delete mode 100644 packages/client-api/src/providers/host/host-provider-config.model.ts delete mode 100644 packages/client-api/src/providers/ip/ip-provider-config.model.ts delete mode 100644 packages/client-api/src/providers/komorebi/komorebi-provider-config.model.ts delete mode 100644 packages/client-api/src/providers/memory/memory-provider-config.model.ts delete mode 100644 packages/client-api/src/providers/monitors/create-monitors-provider.ts delete mode 100644 packages/client-api/src/providers/monitors/monitors-provider-config.model.ts delete mode 100644 packages/client-api/src/providers/network/network-provider-config.model.ts delete mode 100644 packages/client-api/src/providers/util/util-provider-config.model.ts delete mode 100644 packages/client-api/src/providers/weather/weather-provider-config.model.ts diff --git a/packages/client-api/src/init.ts b/packages/client-api/src/init.ts index d6372caf..428959cc 100644 --- a/packages/client-api/src/init.ts +++ b/packages/client-api/src/init.ts @@ -29,7 +29,9 @@ export async function initAsync(): Promise { // @ts-ignore - TODO return { + // @ts-ignore - TODO config: initialState.config, + // @ts-ignore - TODO providers: initialState.providers, openWindow, createProvider: () => {}, diff --git a/packages/client-api/src/providers/battery/battery-provider-config.model.ts b/packages/client-api/src/providers/battery/battery-provider-config.model.ts deleted file mode 100644 index 4f2851ac..00000000 --- a/packages/client-api/src/providers/battery/battery-provider-config.model.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { z } from 'zod'; - -import { ProviderType } from '../provider-type.model'; - -export const BatteryProviderConfigSchema = z.object({ - type: z.literal(ProviderType.BATTERY), - - refresh_interval: z.coerce.number().default(5 * 1000), -}); - -export type BatteryProviderConfig = z.infer< - typeof BatteryProviderConfigSchema ->; diff --git a/packages/client-api/src/providers/battery/create-battery-provider.ts b/packages/client-api/src/providers/battery/create-battery-provider.ts index 2093e0d7..619f5f3e 100644 --- a/packages/client-api/src/providers/battery/create-battery-provider.ts +++ b/packages/client-api/src/providers/battery/create-battery-provider.ts @@ -1,7 +1,13 @@ import type { Owner } from 'solid-js'; -import type { BatteryProviderConfig } from '~/user-config'; import { createProviderListener } from '../create-provider-listener'; +import type { ProviderType } from '../provider-type.model'; + +export interface BatteryProviderConfig { + type: ProviderType.BATTERY; + + refresh_interval: number; +} export interface BatteryVariables { chargePercent: number; diff --git a/packages/client-api/src/providers/cpu/cpu-provider-config.model.ts b/packages/client-api/src/providers/cpu/cpu-provider-config.model.ts deleted file mode 100644 index 76fb3f5d..00000000 --- a/packages/client-api/src/providers/cpu/cpu-provider-config.model.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { z } from 'zod'; - -import { ProviderType } from '../provider-type.model'; - -export const CpuProviderConfigSchema = z.object({ - type: z.literal(ProviderType.CPU), - - refresh_interval: z.coerce.number().default(5 * 1000), -}); - -export type CpuProviderConfig = z.infer; diff --git a/packages/client-api/src/providers/cpu/create-cpu-provider.ts b/packages/client-api/src/providers/cpu/create-cpu-provider.ts index 32152017..b929ebe1 100644 --- a/packages/client-api/src/providers/cpu/create-cpu-provider.ts +++ b/packages/client-api/src/providers/cpu/create-cpu-provider.ts @@ -1,7 +1,13 @@ import type { Owner } from 'solid-js'; -import type { CpuProviderConfig } from '~/user-config'; import { createProviderListener } from '../create-provider-listener'; +import { ProviderType } from '../provider-type.model'; + +export interface CpuProviderConfig { + type: ProviderType.CPU; + + refresh_interval: number; +} export interface CpuVariables { frequency: number; diff --git a/packages/client-api/src/providers/create-provider-listener.ts b/packages/client-api/src/providers/create-provider-listener.ts index c563a283..2bb14369 100644 --- a/packages/client-api/src/providers/create-provider-listener.ts +++ b/packages/client-api/src/providers/create-provider-listener.ts @@ -12,8 +12,8 @@ import { listenProvider, unlistenProvider, } from '~/desktop'; -import type { ProviderConfig } from '~/user-config'; import { simpleHash } from '~/utils'; +import type { ProviderConfig } from './provider-config.model'; /** * Utility for listening to a provider of a given config type. diff --git a/packages/client-api/src/providers/create-provider.ts b/packages/client-api/src/providers/create-provider.ts index c0584289..8f5a0578 100644 --- a/packages/client-api/src/providers/create-provider.ts +++ b/packages/client-api/src/providers/create-provider.ts @@ -8,7 +8,6 @@ import { createHostProvider } from './host/create-host-provider'; import { createIpProvider } from './ip/create-ip-provider'; import { createKomorebiProvider } from './komorebi/create-komorebi-provider'; import { createMemoryProvider } from './memory/create-memory-provider'; -import { createMonitorsProvider } from './monitors/create-monitors-provider'; import { createNetworkProvider } from './network/create-network-provider'; import { createUtilProvider } from './util/create-util-provider'; import { createWeatherProvider } from './weather/create-weather-provider'; @@ -36,8 +35,6 @@ export async function createProvider( return createKomorebiProvider(config, owner); case ProviderType.MEMORY: return createMemoryProvider(config, owner); - case ProviderType.MONITORS: - return createMonitorsProvider(config, owner); case ProviderType.NETWORK: return createNetworkProvider(config, owner); case ProviderType.UTIL: diff --git a/packages/client-api/src/providers/date/create-date-provider.ts b/packages/client-api/src/providers/date/create-date-provider.ts index b3f3001a..b8ccd181 100644 --- a/packages/client-api/src/providers/date/create-date-provider.ts +++ b/packages/client-api/src/providers/date/create-date-provider.ts @@ -2,7 +2,30 @@ import { DateTime } from 'luxon'; import { type Owner, onCleanup, runWithOwner } from 'solid-js'; import { createStore } from 'solid-js/store'; -import type { DateProviderConfig } from '~/user-config'; +import type { ProviderType } from '../provider-type.model'; + +export interface DateProviderConfig { + type: ProviderType.DATE; + + refresh_interval: number; + + /** + * Either a UTC offset (eg. `UTC+8`) or an IANA timezone (eg. + * `America/New_York`). Affects the output of `toFormat()`. + * + * A full list of available IANA timezones can be found [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List). + */ + timezone?: string; + + /** + * An ISO-639-1 locale, which is either a 2-letter language code (eg. `en`) or + * 4-letter language + country code (eg. `en-gb`). Affects the output of + * `toFormat()`. + * + * A full list of ISO-639-1 locales can be found [here](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes#Table). + */ + locale?: string; +} export interface DateVariables { /** diff --git a/packages/client-api/src/providers/date/date-provider-config.model.ts b/packages/client-api/src/providers/date/date-provider-config.model.ts deleted file mode 100644 index 07e6ed82..00000000 --- a/packages/client-api/src/providers/date/date-provider-config.model.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { z } from 'zod'; - -import { ProviderType } from '../provider-type.model'; - -export const DateProviderConfigSchema = z.object({ - type: z.literal(ProviderType.DATE), - - refresh_interval: z.coerce.number().default(1000), - - /** - * Either a UTC offset (eg. `UTC+8`) or an IANA timezone (eg. - * `America/New_York`). Affects the output of `toFormat()`. - * - * A full list of available IANA timezones can be found [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List). - */ - timezone: z.string().optional(), - - /** - * An ISO-639-1 locale, which is either a 2-letter language code (eg. `en`) or - * 4-letter language + country code (eg. `en-gb`). Affects the output of - * `toFormat()`. - * - * A full list of ISO-639-1 locales can be found [here](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes#Table). - */ - locale: z.string().optional(), -}); - -export type DateProviderConfig = z.infer; diff --git a/packages/client-api/src/providers/glazewm/create-glazewm-provider.ts b/packages/client-api/src/providers/glazewm/create-glazewm-provider.ts index 1bc69911..f2a7e249 100644 --- a/packages/client-api/src/providers/glazewm/create-glazewm-provider.ts +++ b/packages/client-api/src/providers/glazewm/create-glazewm-provider.ts @@ -18,8 +18,12 @@ import { createEffect, on, runWithOwner, type Owner } from 'solid-js'; import { createStore } from 'solid-js/store'; import { getMonitors } from '~/desktop'; -import type { GlazewmProviderConfig } from '~/user-config'; import { getCoordinateDistance } from '~/utils'; +import { ProviderType } from '../provider-type.model'; + +export interface GlazeWmProviderConfig { + type: ProviderType.GLAZEWM; +} export interface GlazeWmProvider { /** @@ -84,7 +88,7 @@ export interface GlazeWmProvider { } export async function createGlazeWmProvider( - _: GlazewmProviderConfig, + _: GlazeWmProviderConfig, owner: Owner, ): Promise { const monitors = await getMonitors(); diff --git a/packages/client-api/src/providers/glazewm/glazewm-provider-config.model.ts b/packages/client-api/src/providers/glazewm/glazewm-provider-config.model.ts deleted file mode 100644 index 1e972ba1..00000000 --- a/packages/client-api/src/providers/glazewm/glazewm-provider-config.model.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { z } from 'zod'; - -import { ProviderType } from '../provider-type.model'; - -export const GlazeWmProviderConfigSchema = z.object({ - type: z.literal(ProviderType.GLAZEWM), -}); - -export type GlazewmProviderConfig = z.infer< - typeof GlazeWmProviderConfigSchema ->; diff --git a/packages/client-api/src/providers/host/create-host-provider.ts b/packages/client-api/src/providers/host/create-host-provider.ts index 51b95c2c..146ab4a4 100644 --- a/packages/client-api/src/providers/host/create-host-provider.ts +++ b/packages/client-api/src/providers/host/create-host-provider.ts @@ -1,7 +1,13 @@ import type { Owner } from 'solid-js'; -import type { HostProviderConfig } from '~/user-config'; import { createProviderListener } from '../create-provider-listener'; +import type { ProviderType } from '../provider-type.model'; + +export interface HostProviderConfig { + type: ProviderType.HOST; + + refresh_interval: number; +} export interface HostVariables { hostname: string | null; diff --git a/packages/client-api/src/providers/host/host-provider-config.model.ts b/packages/client-api/src/providers/host/host-provider-config.model.ts deleted file mode 100644 index 6a87cace..00000000 --- a/packages/client-api/src/providers/host/host-provider-config.model.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { z } from 'zod'; - -import { ProviderType } from '../provider-type.model'; - -export const HostProviderConfigSchema = z.object({ - type: z.literal(ProviderType.HOST), - - refresh_interval: z.coerce.number().default(60 * 1000), -}); - -export type HostProviderConfig = z.infer; diff --git a/packages/client-api/src/providers/ip/create-ip-provider.ts b/packages/client-api/src/providers/ip/create-ip-provider.ts index ded4ec02..c452ee22 100644 --- a/packages/client-api/src/providers/ip/create-ip-provider.ts +++ b/packages/client-api/src/providers/ip/create-ip-provider.ts @@ -1,7 +1,13 @@ import type { Owner } from 'solid-js'; -import type { IpProviderConfig } from '~/user-config'; import { createProviderListener } from '../create-provider-listener'; +import type { ProviderType } from '../provider-type.model'; + +export interface IpProviderConfig { + type: ProviderType.IP; + + refresh_interval: number; +} export interface IpVariables { address: string; diff --git a/packages/client-api/src/providers/ip/ip-provider-config.model.ts b/packages/client-api/src/providers/ip/ip-provider-config.model.ts deleted file mode 100644 index a1c674ee..00000000 --- a/packages/client-api/src/providers/ip/ip-provider-config.model.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { z } from 'zod'; - -import { ProviderType } from '../provider-type.model'; - -export const IpProviderConfigSchema = z.object({ - type: z.literal(ProviderType.IP), - - refresh_interval: z.coerce.number().default(60 * 60 * 1000), -}); - -export type IpProviderConfig = z.infer; diff --git a/packages/client-api/src/providers/komorebi/create-komorebi-provider.ts b/packages/client-api/src/providers/komorebi/create-komorebi-provider.ts index e498f64f..c4a6e185 100644 --- a/packages/client-api/src/providers/komorebi/create-komorebi-provider.ts +++ b/packages/client-api/src/providers/komorebi/create-komorebi-provider.ts @@ -1,10 +1,14 @@ import { createEffect, runWithOwner, type Owner } from 'solid-js'; import { createStore } from 'solid-js/store'; -import type { KomorebiProviderConfig } from '~/user-config'; import { createProviderListener } from '../create-provider-listener'; import { getMonitors } from '~/desktop'; import { getCoordinateDistance } from '~/utils'; +import { ProviderType } from '../provider-type.model'; + +export interface KomorebiProviderConfig { + type: ProviderType.KOMOREBI; +} interface KomorebiResponse { allMonitors: KomorebiMonitor[]; diff --git a/packages/client-api/src/providers/komorebi/komorebi-provider-config.model.ts b/packages/client-api/src/providers/komorebi/komorebi-provider-config.model.ts deleted file mode 100644 index 451562af..00000000 --- a/packages/client-api/src/providers/komorebi/komorebi-provider-config.model.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { z } from 'zod'; - -import { ProviderType } from '../provider-type.model'; - -export const KomorebiProviderConfigSchema = z.object({ - type: z.literal(ProviderType.KOMOREBI), -}); - -export type KomorebiProviderConfig = z.infer< - typeof KomorebiProviderConfigSchema ->; diff --git a/packages/client-api/src/providers/memory/create-memory-provider.ts b/packages/client-api/src/providers/memory/create-memory-provider.ts index 00302129..ccb28c2c 100644 --- a/packages/client-api/src/providers/memory/create-memory-provider.ts +++ b/packages/client-api/src/providers/memory/create-memory-provider.ts @@ -1,7 +1,13 @@ import type { Owner } from 'solid-js'; -import type { MemoryProviderConfig } from '~/user-config'; import { createProviderListener } from '../create-provider-listener'; +import type { ProviderType } from '../provider-type.model'; + +export interface MemoryProviderConfig { + type: ProviderType.MEMORY; + + refresh_interval: number; +} export interface MemoryVariables { usage: number; diff --git a/packages/client-api/src/providers/memory/memory-provider-config.model.ts b/packages/client-api/src/providers/memory/memory-provider-config.model.ts deleted file mode 100644 index 93a8035d..00000000 --- a/packages/client-api/src/providers/memory/memory-provider-config.model.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { z } from 'zod'; - -import { ProviderType } from '../provider-type.model'; - -export const MemoryProviderConfigSchema = z.object({ - type: z.literal(ProviderType.MEMORY), - - refresh_interval: z.coerce.number().default(5 * 1000), -}); - -export type MemoryProviderConfig = z.infer< - typeof MemoryProviderConfigSchema ->; diff --git a/packages/client-api/src/providers/monitors/create-monitors-provider.ts b/packages/client-api/src/providers/monitors/create-monitors-provider.ts deleted file mode 100644 index 37b63bc5..00000000 --- a/packages/client-api/src/providers/monitors/create-monitors-provider.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { Owner } from 'solid-js'; - -import { type MonitorInfo, getMonitors } from '~/desktop'; -import type { MonitorsProviderConfig } from '~/user-config'; - -export interface MonitorsVariables { - primary?: MonitorInfo; - secondary: MonitorInfo[]; - all: MonitorInfo[]; -} - -export async function createMonitorsProvider( - _: MonitorsProviderConfig, - __: Owner, -) { - const { primaryMonitor, secondaryMonitors, allMonitors } = - await getMonitors(); - - return { - get primary() { - return primaryMonitor; - }, - get secondary() { - return secondaryMonitors; - }, - get all() { - return allMonitors; - }, - }; -} diff --git a/packages/client-api/src/providers/monitors/monitors-provider-config.model.ts b/packages/client-api/src/providers/monitors/monitors-provider-config.model.ts deleted file mode 100644 index 69cf1f53..00000000 --- a/packages/client-api/src/providers/monitors/monitors-provider-config.model.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { z } from 'zod'; - -import { ProviderType } from '../provider-type.model'; - -export const MonitorsProviderConfigSchema = z.object({ - type: z.literal(ProviderType.MONITORS), -}); - -export type MonitorsProviderConfig = z.infer< - typeof MonitorsProviderConfigSchema ->; diff --git a/packages/client-api/src/providers/network/create-network-provider.ts b/packages/client-api/src/providers/network/create-network-provider.ts index 712de622..a922ed9e 100644 --- a/packages/client-api/src/providers/network/create-network-provider.ts +++ b/packages/client-api/src/providers/network/create-network-provider.ts @@ -1,7 +1,13 @@ import type { Owner } from 'solid-js'; -import type { NetworkProviderConfig } from '~/user-config'; import { createProviderListener } from '../create-provider-listener'; +import type { ProviderType } from '../provider-type.model'; + +export interface NetworkProviderConfig { + type: ProviderType.NETWORK; + + refresh_interval: number; +} export interface NetworkVariables { defaultInterface: NetworkInterface | null; diff --git a/packages/client-api/src/providers/network/network-provider-config.model.ts b/packages/client-api/src/providers/network/network-provider-config.model.ts deleted file mode 100644 index 2580630d..00000000 --- a/packages/client-api/src/providers/network/network-provider-config.model.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { z } from 'zod'; - -import { ProviderType } from '../provider-type.model'; - -export const NetworkProviderConfigSchema = z.object({ - type: z.literal(ProviderType.NETWORK), - - refresh_interval: z.coerce.number().default(5 * 1000), -}); - -export type NetworkProviderConfig = z.infer< - typeof NetworkProviderConfigSchema ->; diff --git a/packages/client-api/src/providers/provider-config.model.ts b/packages/client-api/src/providers/provider-config.model.ts index 7adfbaa0..6ff2acc6 100644 --- a/packages/client-api/src/providers/provider-config.model.ts +++ b/packages/client-api/src/providers/provider-config.model.ts @@ -1,38 +1,24 @@ -import { z } from 'zod'; +import type { BatteryProviderConfig } from './battery/create-battery-provider'; +import type { CpuProviderConfig } from './cpu/create-cpu-provider'; +import type { DateProviderConfig } from './date/create-date-provider'; +import type { GlazeWmProviderConfig } from './glazewm/create-glazewm-provider'; +import type { HostProviderConfig } from './host/create-host-provider'; +import type { IpProviderConfig } from './ip/create-ip-provider'; +import type { KomorebiProviderConfig } from './komorebi/create-komorebi-provider'; +import type { MemoryProviderConfig } from './memory/create-memory-provider'; +import type { NetworkProviderConfig } from './network/create-network-provider'; +import type { UtilProviderConfig } from './util/create-util-provider'; +import type { WeatherProviderConfig } from './weather/create-weather-provider'; -import type { Prettify } from '~/utils'; -import { - BatteryProviderConfigSchema, - CpuProviderConfigSchema, - DateProviderConfigSchema, - GlazeWmProviderConfigSchema, - HostProviderConfigSchema, - IpProviderConfigSchema, - KomorebiProviderConfigSchema, - MemoryProviderConfigSchema, - MonitorsProviderConfigSchema, - NetworkProviderConfigSchema, - SelfProviderConfigSchema, - UtilProviderConfigSchema, - WeatherProviderConfigSchema, -} from './providers'; - -export const ProviderConfigSchema = z.union([ - BatteryProviderConfigSchema, - CpuProviderConfigSchema, - DateProviderConfigSchema, - GlazeWmProviderConfigSchema, - HostProviderConfigSchema, - IpProviderConfigSchema, - KomorebiProviderConfigSchema, - MemoryProviderConfigSchema, - MonitorsProviderConfigSchema, - NetworkProviderConfigSchema, - SelfProviderConfigSchema, - UtilProviderConfigSchema, - WeatherProviderConfigSchema, -]); - -export type ProviderConfig = Prettify< - z.infer ->; +export type ProviderConfig = + | BatteryProviderConfig + | CpuProviderConfig + | DateProviderConfig + | GlazeWmProviderConfig + | HostProviderConfig + | IpProviderConfig + | KomorebiProviderConfig + | MemoryProviderConfig + | NetworkProviderConfig + | UtilProviderConfig + | WeatherProviderConfig; diff --git a/packages/client-api/src/providers/provider-type.model.ts b/packages/client-api/src/providers/provider-type.model.ts index 75ec37a0..4cefe5bc 100644 --- a/packages/client-api/src/providers/provider-type.model.ts +++ b/packages/client-api/src/providers/provider-type.model.ts @@ -9,7 +9,6 @@ export enum ProviderType { IP = 'ip', KOMOREBI = 'komorebi', MEMORY = 'memory', - MONITORS = 'monitors', NETWORK = 'network', UTIL = 'util', WEATHER = 'weather', diff --git a/packages/client-api/src/providers/util/create-util-provider.ts b/packages/client-api/src/providers/util/create-util-provider.ts index 1f4ced90..6ea795b0 100644 --- a/packages/client-api/src/providers/util/create-util-provider.ts +++ b/packages/client-api/src/providers/util/create-util-provider.ts @@ -1,6 +1,10 @@ import type { Owner } from 'solid-js'; -import type { UtilProviderConfig } from '~/user-config'; +import type { ProviderType } from '../provider-type.model'; + +export interface UtilProviderConfig { + type: ProviderType.UTIL; +} export enum DataUnit { BITS = 'bits', diff --git a/packages/client-api/src/providers/util/util-provider-config.model.ts b/packages/client-api/src/providers/util/util-provider-config.model.ts deleted file mode 100644 index 6cb4ec3b..00000000 --- a/packages/client-api/src/providers/util/util-provider-config.model.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { z } from 'zod'; - -import { ProviderType } from '../provider-type.model'; - -export const UtilProviderConfigSchema = z.object({ - type: z.literal(ProviderType.UTIL), -}); - -export type UtilProviderConfig = z.infer; diff --git a/packages/client-api/src/providers/weather/create-weather-provider.ts b/packages/client-api/src/providers/weather/create-weather-provider.ts index ab6f4b5f..4c699a79 100644 --- a/packages/client-api/src/providers/weather/create-weather-provider.ts +++ b/packages/client-api/src/providers/weather/create-weather-provider.ts @@ -1,12 +1,33 @@ import type { Owner } from 'solid-js'; -import { ProviderType, type WeatherProviderConfig } from '~/user-config'; import { type IpVariables, createIpProvider, } from '../ip/create-ip-provider'; import { WeatherStatus } from './weather-status.enum'; import { createProviderListener } from '../create-provider-listener'; +import { ProviderType } from '../provider-type.model'; + +export interface WeatherProviderConfig { + type: ProviderType.WEATHER; + + /** + * Latitude to retrieve weather for. If not provided, latitude is instead + * estimated based on public IP. + */ + latitude?: number; + + /** + * Longitude to retrieve weather for. If not provided, longitude is instead + * estimated based on public IP. + */ + longitude?: number; + + /** + * How often this provider refreshes in milliseconds. + */ + refresh_interval?: number; +} export interface WeatherVariables { isDaytime: boolean; diff --git a/packages/client-api/src/providers/weather/weather-provider-config.model.ts b/packages/client-api/src/providers/weather/weather-provider-config.model.ts deleted file mode 100644 index 35e61ab3..00000000 --- a/packages/client-api/src/providers/weather/weather-provider-config.model.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { z } from 'zod'; - -import { ProviderType } from '../provider-type.model'; - -export const WeatherProviderConfigSchema = z.object({ - type: z.literal(ProviderType.WEATHER), - - /** - * Latitude to retrieve weather for. If not provided, latitude is instead - * estimated based on public IP. - */ - latitude: z.coerce.number().optional(), - - /** - * Longitude to retrieve weather for. If not provided, longitude is instead - * estimated based on public IP. - */ - longitude: z.coerce.number().optional(), - - /** - * How often this component refreshes in milliseconds. - */ - refresh_interval: z.coerce.number().default(60 * 60 * 1000), -}); - -export type WeatherProviderConfig = z.infer< - typeof WeatherProviderConfigSchema ->; diff --git a/packages/client/index.html b/packages/client/index.html index ac00cdb3..495ff786 100644 --- a/packages/client/index.html +++ b/packages/client/index.html @@ -1,16 +1,20 @@ - - - - - - - - Zebar - + - -

+ + async function App(ctx, inputs) { + const { cpu, memory } = await ctx.createProviders([ + 'cpu', + 'memory', + ]); + + return html`
${cpu.usage}
`; + } + + init().then(ctx => render(App(ctx), document.body)); + From 707e1123a71d7e6e79aa69700640152d6c46727f Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 22 Aug 2024 19:47:45 +0800 Subject: [PATCH 011/138] refactor: remove client app --- .../src/app/child-element.component.tsx | 34 ----- .../src/app/group-element.component.tsx | 33 ----- .../src/app/template-element.component.ts | 116 ------------------ .../src/app/window-element.component.tsx | 35 ------ packages/client/src/index.tsx | 11 -- 5 files changed, 229 deletions(-) delete mode 100644 packages/client/src/app/child-element.component.tsx delete mode 100644 packages/client/src/app/group-element.component.tsx delete mode 100644 packages/client/src/app/template-element.component.ts delete mode 100644 packages/client/src/app/window-element.component.tsx diff --git a/packages/client/src/app/child-element.component.tsx b/packages/client/src/app/child-element.component.tsx deleted file mode 100644 index 9f05795c..00000000 --- a/packages/client/src/app/child-element.component.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Match, Show, Switch, createSignal } from 'solid-js'; -import { type ElementContext, ElementType } from 'zebar'; - -import { TemplateElement } from './template-element.component'; -import { GroupElement } from './group-element.component'; - -export interface ChildElementProps { - childId: string; - parentContext: ElementContext; -} - -export function ChildElement(props: ChildElementProps) { - const [childContext, setChildContext] = - createSignal(null); - - props.parentContext - .initChildElement(props.childId) - .then(setChildContext); - - return ( - - {context => ( - - - - - - - - - )} - - ); -} diff --git a/packages/client/src/app/group-element.component.tsx b/packages/client/src/app/group-element.component.tsx deleted file mode 100644 index 54a74dc5..00000000 --- a/packages/client/src/app/group-element.component.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Index } from 'solid-js'; -import { - type ElementContext, - getChildConfigs, - toCssSelector, -} from 'zebar'; - -import { ChildElement } from './child-element.component'; - -export interface GroupElementProps { - context: ElementContext; -} - -export function GroupElement(props: GroupElementProps) { - const config = props.context.parsedConfig; - const rawConfig = props.context.rawConfig; - - return ( -
- - {childConfig => ( - - )} - -
- ); -} diff --git a/packages/client/src/app/template-element.component.ts b/packages/client/src/app/template-element.component.ts deleted file mode 100644 index 4af2cd04..00000000 --- a/packages/client/src/app/template-element.component.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { createEffect, onCleanup, onMount } from 'solid-js'; -import { - type ElementContext, - createLogger, - toCssSelector, - getScriptManager, -} from 'zebar'; -import morphdom from 'morphdom'; - -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}`); - const scriptManager = getScriptManager(); - - // Create element with ID. - const element = createRootElement(); - - // Currently active event listeners. - let listeners: ElementEventListener[] = []; - - createEffect(() => { - // Subsequent template updates after the initial render - - // Since templates do not include the root template element, - // copy the existing one without its children. - const templateRoot = element.cloneNode(false) as Element; - - // Insert the template into the cloned root element - templateRoot.innerHTML = (config as any).template; - - try { - // Reconcile the DOM with the updated template - // @ts-ignore - TODO: fix config.template type - morphdom(element, templateRoot, { - // Don't morph fromNode or toNode, only their children - childrenOnly: true, - }); - } catch (error) { - // TODO - add error handling for reconciliation here - logger.error( - `Failed to reconciliate ${props.context.id} template:`, - error, - ); - } - - updateEventListeners(); - }); - - onMount(() => { - logger.debug('Mounted'); - try { - // Initial render, set innerHTML to the template - // @ts-ignore - TODO: fix config.template type - element.innerHTML = config.template; - } catch (error) { - logger.error( - `Initial render of ${[props.context.id]} failed:`, - error, - ); - } - }); - onCleanup(() => logger.debug('Cleanup')); - - function createRootElement() { - const element = document.createElement('div'); - element.className = config.class_names.join(' '); - element.id = toCssSelector(config.id); - return element; - } - - function updateEventListeners() { - // Remove existing event listeners. - listeners.forEach(({ eventType, eventCallback, selectorElement }) => - selectorElement.removeEventListener(eventType, eventCallback), - ); - - listeners = []; - - config.events.forEach(eventConfig => { - const eventCallback = (event: Event) => - scriptManager.callFn(eventConfig.fn_path, event, props.context); - - // Default to the root element if no selector is provided. - const selectorElements = eventConfig.selector - ? Array.from(element.querySelectorAll(eventConfig.selector)) - : [element]; - - for (const selectorElement of selectorElements) { - if (selectorElement) { - selectorElement.addEventListener( - eventConfig.type, - eventCallback, - ); - - listeners.push({ - eventType: eventConfig.type, - eventCallback, - selectorElement, - }); - } - } - }); - } - - return element; -} diff --git a/packages/client/src/app/window-element.component.tsx b/packages/client/src/app/window-element.component.tsx deleted file mode 100644 index 4d5eca76..00000000 --- a/packages/client/src/app/window-element.component.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Index, Show, createSignal } from 'solid-js'; -import { - type WindowContext, - getChildConfigs, - initWindow, - toCssSelector, -} from 'zebar'; - -import { ChildElement } from './child-element.component'; - -export function WindowElement() { - const [context, setContext] = createSignal(null); - - initWindow(context => setContext(context)); - - return ( - - {context => ( -
- - {childConfig => ( - - )} - -
- )} -
- ); -} diff --git a/packages/client/src/index.tsx b/packages/client/src/index.tsx index e254fc82..7a93175b 100644 --- a/packages/client/src/index.tsx +++ b/packages/client/src/index.tsx @@ -1,14 +1,3 @@ /* @refresh reload */ -import { render } from 'solid-js/web'; - import './normalize.css'; import './index.css'; -import { WindowElement } from './app/window-element.component'; - -const root = document.getElementById('zebar'); - -if (import.meta.env.DEV && !(root instanceof HTMLElement)) { - throw new Error('Root element not found.'); -} - -render(() => , root!); From 51cf0a96f13fe05f11692ff64d3a3e6fd832d3ab Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Fri, 23 Aug 2024 00:35:40 +0800 Subject: [PATCH 012/138] feat: improvements to ts typing of provider fns --- packages/client-api/src/init.ts | 10 +- .../battery/create-battery-provider.ts | 9 +- .../src/providers/cpu/create-cpu-provider.ts | 9 +- .../src/providers/create-provider.ts | 210 ++++++++++++++---- .../providers/date/create-date-provider.ts | 25 ++- .../providers/host/create-host-provider.ts | 9 +- .../src/providers/ip/create-ip-provider.ts | 9 +- .../komorebi/create-komorebi-provider.ts | 10 +- .../memory/create-memory-provider.ts | 9 +- .../network/create-network-provider.ts | 9 +- .../providers/util/create-util-provider.ts | 10 +- .../weather/create-weather-provider.ts | 15 +- 12 files changed, 259 insertions(+), 75 deletions(-) diff --git a/packages/client-api/src/init.ts b/packages/client-api/src/init.ts index 428959cc..84cb8e02 100644 --- a/packages/client-api/src/init.ts +++ b/packages/client-api/src/init.ts @@ -4,6 +4,7 @@ import { createRoot, getOwner, runWithOwner } from 'solid-js'; import { getInitialState, openWindow, showErrorDialog } from './desktop'; import { createLogger } from '~/utils'; import type { ZebarContext } from './zebar-context.model'; +import { createStore } from 'solid-js/store'; const logger = createLogger('init-window'); @@ -25,14 +26,19 @@ export async function initAsync(): Promise { window.__ZEBAR_INITIAL_STATE ?? (await getInitialState(currentWindow.label)); + const providers = createStore([]); + await currentWindow.show(); + function createProvider() { + // TODO + } + // @ts-ignore - TODO return { // @ts-ignore - TODO config: initialState.config, - // @ts-ignore - TODO - providers: initialState.providers, + providers, openWindow, createProvider: () => {}, currentWindow: {}, diff --git a/packages/client-api/src/providers/battery/create-battery-provider.ts b/packages/client-api/src/providers/battery/create-battery-provider.ts index 619f5f3e..17e0fd27 100644 --- a/packages/client-api/src/providers/battery/create-battery-provider.ts +++ b/packages/client-api/src/providers/battery/create-battery-provider.ts @@ -6,10 +6,13 @@ import type { ProviderType } from '../provider-type.model'; export interface BatteryProviderConfig { type: ProviderType.BATTERY; - refresh_interval: number; + /** + * How often this provider refreshes in milliseconds. + */ + refreshInterval?: number; } -export interface BatteryVariables { +export interface BatteryProvider { chargePercent: number; cycleCount: number; healthPercent: number; @@ -27,7 +30,7 @@ export async function createBatteryProvider( ) { const batteryVariables = await createProviderListener< BatteryProviderConfig, - BatteryVariables + BatteryProvider >(config, owner); return { diff --git a/packages/client-api/src/providers/cpu/create-cpu-provider.ts b/packages/client-api/src/providers/cpu/create-cpu-provider.ts index b929ebe1..ee4b45ed 100644 --- a/packages/client-api/src/providers/cpu/create-cpu-provider.ts +++ b/packages/client-api/src/providers/cpu/create-cpu-provider.ts @@ -6,10 +6,13 @@ import { ProviderType } from '../provider-type.model'; export interface CpuProviderConfig { type: ProviderType.CPU; - refresh_interval: number; + /** + * How often this provider refreshes in milliseconds. + */ + refreshInterval?: number; } -export interface CpuVariables { +export interface CpuProvider { frequency: number; usage: number; logicalCoreCount: number; @@ -23,7 +26,7 @@ export async function createCpuProvider( ) { const cpuVariables = await createProviderListener< CpuProviderConfig, - CpuVariables + CpuProvider >(config, owner); return { diff --git a/packages/client-api/src/providers/create-provider.ts b/packages/client-api/src/providers/create-provider.ts index 8f5a0578..cda151a9 100644 --- a/packages/client-api/src/providers/create-provider.ts +++ b/packages/client-api/src/providers/create-provider.ts @@ -1,47 +1,179 @@ import type { Owner } from 'solid-js'; -import { createBatteryProvider } from './battery/create-battery-provider'; -import { createCpuProvider } from './cpu/create-cpu-provider'; -import { createDateProvider } from './date/create-date-provider'; -import { createGlazeWmProvider } from './glazewm/create-glazewm-provider'; -import { createHostProvider } from './host/create-host-provider'; -import { createIpProvider } from './ip/create-ip-provider'; -import { createKomorebiProvider } from './komorebi/create-komorebi-provider'; -import { createMemoryProvider } from './memory/create-memory-provider'; -import { createNetworkProvider } from './network/create-network-provider'; -import { createUtilProvider } from './util/create-util-provider'; -import { createWeatherProvider } from './weather/create-weather-provider'; +import { + createBatteryProvider, + type BatteryProvider, +} from './battery/create-battery-provider'; +import { + createCpuProvider, + type CpuProvider, +} from './cpu/create-cpu-provider'; +import { + createDateProvider, + type DateProvider, +} from './date/create-date-provider'; +import { + createGlazeWmProvider, + type GlazeWmProvider, +} from './glazewm/create-glazewm-provider'; +import { + createHostProvider, + type HostProvider, +} from './host/create-host-provider'; +import { + createIpProvider, + type IpProvider, +} from './ip/create-ip-provider'; +import { + createKomorebiProvider, + type KomorebiProvider, +} from './komorebi/create-komorebi-provider'; +import { + createMemoryProvider, + type MemoryProvider, +} from './memory/create-memory-provider'; +import { + createNetworkProvider, + type NetworkProvider, +} from './network/create-network-provider'; +import { + createUtilProvider, + type UtilProvider, +} from './util/create-util-provider'; +import { + createWeatherProvider, + type WeatherProvider, +} from './weather/create-weather-provider'; import type { ProviderConfig } from './provider-config.model'; import { ProviderType } from './provider-type.model'; -export async function createProvider( - config: ProviderConfig, +// type ProviderTypeToProvider = { +// [ProviderType.BATTERY]: BatteryProvider; +// [ProviderType.CPU]: CpuProvider; +// [ProviderType.DATE]: DateProvider; +// [ProviderType.GLAZEWM]: GlazeWmProvider; +// [ProviderType.HOST]: HostProvider; +// [ProviderType.IP]: IpProvider; +// [ProviderType.KOMOREBI]: KomorebiProvider; +// [ProviderType.MEMORY]: MemoryProvider; +// [ProviderType.NETWORK]: NetworkProvider; +// [ProviderType.UTIL]: UtilProvider; +// [ProviderType.WEATHER]: WeatherProvider; +// }; + +async function aa() { + const xx = await createProvider({ type: ProviderType.UTIL }, {} as any); +} + +// const createProviderMap = { +// [ProviderType.BATTERY]: createBatteryProvider, +// [ProviderType.CPU]: createCpuProvider, +// [ProviderType.DATE]: createDateProvider, +// [ProviderType.GLAZEWM]: createGlazeWmProvider, +// [ProviderType.HOST]: createHostProvider, +// [ProviderType.IP]: createIpProvider, +// [ProviderType.KOMOREBI]: createKomorebiProvider, +// [ProviderType.MEMORY]: createMemoryProvider, +// [ProviderType.NETWORK]: createNetworkProvider, +// [ProviderType.UTIL]: createUtilProvider, +// [ProviderType.WEATHER]: createWeatherProvider, +// } as const; + +// type ProviderCreator = ( +// config: T, +// owner: Owner, +// ) => Promise; + +// type ProviderCreators = { +// [T in ProviderType]: ProviderCreator< +// Extract +// >; +// }; + +// const providerCreators: ProviderCreators = { +// [ProviderType.BATTERY]: createBatteryProvider, +// [ProviderType.CPU]: createCpuProvider, +// [ProviderType.DATE]: createDateProvider, +// [ProviderType.GLAZEWM]: createGlazeWmProvider, +// [ProviderType.HOST]: createHostProvider, +// [ProviderType.IP]: createIpProvider, +// [ProviderType.KOMOREBI]: createKomorebiProvider, +// [ProviderType.MEMORY]: createMemoryProvider, +// [ProviderType.NETWORK]: createNetworkProvider, +// [ProviderType.UTIL]: createUtilProvider, +// [ProviderType.WEATHER]: createWeatherProvider, +// }; + +// export async function createProvider( +// config: T, +// owner: Owner, +// ): Promise> { +// const creator = providerCreators[config.type] as ProviderCreator; + +// if (!creator) { +// throw new Error('Not a supported provider type.'); +// } + +// return creator(config, owner); +// } + +// ): Promise { +// ) { +// const createProviderMap = { +// [ProviderType.BATTERY]: createBatteryProvider, +// [ProviderType.CPU]: createCpuProvider, +// [ProviderType.DATE]: createDateProvider, +// [ProviderType.GLAZEWM]: createGlazeWmProvider, +// [ProviderType.HOST]: createHostProvider, +// [ProviderType.IP]: createIpProvider, +// [ProviderType.KOMOREBI]: createKomorebiProvider, +// [ProviderType.MEMORY]: createMemoryProvider, +// [ProviderType.NETWORK]: createNetworkProvider, +// [ProviderType.UTIL]: createUtilProvider, +// [ProviderType.WEATHER]: createWeatherProvider, +// } as const; + +// return createProviderMap[config.type](config as any, owner); +// // const providerFn = createProviderMap[config.type] as ( +// // config: any, +// // owner: Owner, +// // ) => Promise; + +// // if (!providerFn) { +// // throw new Error('Not a supported provider type.'); +// // } + +// // return providerFn(config, owner); +// } + +const providerCreators = { + [ProviderType.BATTERY]: createBatteryProvider, + [ProviderType.CPU]: createCpuProvider, + [ProviderType.DATE]: createDateProvider, + [ProviderType.GLAZEWM]: createGlazeWmProvider, + [ProviderType.HOST]: createHostProvider, + [ProviderType.IP]: createIpProvider, + [ProviderType.KOMOREBI]: createKomorebiProvider, + [ProviderType.MEMORY]: createMemoryProvider, + [ProviderType.NETWORK]: createNetworkProvider, + [ProviderType.UTIL]: createUtilProvider, + [ProviderType.WEATHER]: createWeatherProvider, +} as const; + +type ProviderCreatorMap = typeof providerCreators; +type ProviderConfigType = ProviderConfig & { + type: keyof ProviderCreatorMap; +}; + +export async function createProvider( + config: T, owner: Owner, -) { - switch (config.type) { - case ProviderType.BATTERY: - return createBatteryProvider(config, owner); - case ProviderType.CPU: - return createCpuProvider(config, owner); - case ProviderType.DATE: - return createDateProvider(config, owner); - case ProviderType.GLAZEWM: - return createGlazeWmProvider(config, owner); - case ProviderType.HOST: - return createHostProvider(config, owner); - case ProviderType.IP: - return createIpProvider(config, owner); - case ProviderType.KOMOREBI: - return createKomorebiProvider(config, owner); - case ProviderType.MEMORY: - return createMemoryProvider(config, owner); - case ProviderType.NETWORK: - return createNetworkProvider(config, owner); - case ProviderType.UTIL: - return createUtilProvider(config, owner); - case ProviderType.WEATHER: - return createWeatherProvider(config, owner); - default: - throw new Error('Not a supported provider type.'); +): Promise> { + const creator = providerCreators[config.type]; + + if (!creator) { + throw new Error('Not a supported provider type.'); } + + return creator(config as any, owner) as any; } diff --git a/packages/client-api/src/providers/date/create-date-provider.ts b/packages/client-api/src/providers/date/create-date-provider.ts index b8ccd181..36f3e91c 100644 --- a/packages/client-api/src/providers/date/create-date-provider.ts +++ b/packages/client-api/src/providers/date/create-date-provider.ts @@ -7,7 +7,10 @@ import type { ProviderType } from '../provider-type.model'; export interface DateProviderConfig { type: ProviderType.DATE; - refresh_interval: number; + /** + * How often this provider refreshes in milliseconds. + */ + refreshInterval?: number; /** * Either a UTC offset (eg. `UTC+8`) or an IANA timezone (eg. @@ -27,7 +30,7 @@ export interface DateProviderConfig { locale?: string; } -export interface DateVariables { +export interface DateProvider { /** * Current date/time as a JavaScript `Date` object. Uses `new Date()` under * the hood. @@ -45,8 +48,24 @@ export interface DateVariables { * `2017-04-22T20:47:05.335-04:00`). Uses `date.toISOString()` under the hood. **/ iso: string; + + /** + * Format a given date/time into a custom string format. + * + * Refer to [table of tokens](https://moment.github.io/luxon/#/formatting?id=table-of-tokens) + * for available date/time tokens. + * + * @example + * `toFormat(now, 'yyyy LLL dd')` -> `2023 Feb 13` + * `toFormat(now, "HH 'hours and' mm 'minutes'")` -> `20 hours and 55 minutes` + * @param now Date/time as milliseconds since epoch. + * @param format Custom string format. + */ + toFormat(now: number, format: string): string; } +type DateVariables = Omit; + export async function createDateProvider( config: DateProviderConfig, owner: Owner, @@ -56,7 +75,7 @@ export async function createDateProvider( const interval = setInterval( () => setDateVariables(getDateVariables()), - config.refresh_interval, + config.refreshInterval, ); runWithOwner(owner, () => { diff --git a/packages/client-api/src/providers/host/create-host-provider.ts b/packages/client-api/src/providers/host/create-host-provider.ts index 146ab4a4..581f2595 100644 --- a/packages/client-api/src/providers/host/create-host-provider.ts +++ b/packages/client-api/src/providers/host/create-host-provider.ts @@ -6,10 +6,13 @@ import type { ProviderType } from '../provider-type.model'; export interface HostProviderConfig { type: ProviderType.HOST; - refresh_interval: number; + /** + * How often this provider refreshes in milliseconds. + */ + refreshInterval?: number; } -export interface HostVariables { +export interface HostProvider { hostname: string | null; osName: string | null; osVersion: string | null; @@ -24,7 +27,7 @@ export async function createHostProvider( ) { const hostVariables = await createProviderListener< HostProviderConfig, - HostVariables + HostProvider >(config, owner); return { diff --git a/packages/client-api/src/providers/ip/create-ip-provider.ts b/packages/client-api/src/providers/ip/create-ip-provider.ts index c452ee22..18e67755 100644 --- a/packages/client-api/src/providers/ip/create-ip-provider.ts +++ b/packages/client-api/src/providers/ip/create-ip-provider.ts @@ -6,10 +6,13 @@ import type { ProviderType } from '../provider-type.model'; export interface IpProviderConfig { type: ProviderType.IP; - refresh_interval: number; + /** + * How often this provider refreshes in milliseconds. + */ + refreshInterval?: number; } -export interface IpVariables { +export interface IpProvider { address: string; approxCity: string; approxCountry: string; @@ -23,7 +26,7 @@ export async function createIpProvider( ) { const ipVariables = await createProviderListener< IpProviderConfig, - IpVariables + IpProvider >(config, owner); return { diff --git a/packages/client-api/src/providers/komorebi/create-komorebi-provider.ts b/packages/client-api/src/providers/komorebi/create-komorebi-provider.ts index c4a6e185..98b1111a 100644 --- a/packages/client-api/src/providers/komorebi/create-komorebi-provider.ts +++ b/packages/client-api/src/providers/komorebi/create-komorebi-provider.ts @@ -10,11 +10,6 @@ export interface KomorebiProviderConfig { type: ProviderType.KOMOREBI; } -interface KomorebiResponse { - allMonitors: KomorebiMonitor[]; - focusedMonitorIndex: number; -} - export interface KomorebiProvider { /** * Workspace displayed on the current monitor. @@ -52,6 +47,11 @@ export interface KomorebiProvider { currentMonitor: KomorebiMonitor; } +interface KomorebiResponse { + allMonitors: KomorebiMonitor[]; + focusedMonitorIndex: number; +} + export interface KomorebiMonitor { id: number; deviceId: string; diff --git a/packages/client-api/src/providers/memory/create-memory-provider.ts b/packages/client-api/src/providers/memory/create-memory-provider.ts index ccb28c2c..7fbb593c 100644 --- a/packages/client-api/src/providers/memory/create-memory-provider.ts +++ b/packages/client-api/src/providers/memory/create-memory-provider.ts @@ -6,10 +6,13 @@ import type { ProviderType } from '../provider-type.model'; export interface MemoryProviderConfig { type: ProviderType.MEMORY; - refresh_interval: number; + /** + * How often this provider refreshes in milliseconds. + */ + refreshInterval?: number; } -export interface MemoryVariables { +export interface MemoryProvider { usage: number; freeMemory: number; usedMemory: number; @@ -25,7 +28,7 @@ export async function createMemoryProvider( ) { const memoryVariables = await createProviderListener< MemoryProviderConfig, - MemoryVariables + MemoryProvider >(config, owner); return { diff --git a/packages/client-api/src/providers/network/create-network-provider.ts b/packages/client-api/src/providers/network/create-network-provider.ts index a922ed9e..94a594fa 100644 --- a/packages/client-api/src/providers/network/create-network-provider.ts +++ b/packages/client-api/src/providers/network/create-network-provider.ts @@ -6,10 +6,13 @@ import type { ProviderType } from '../provider-type.model'; export interface NetworkProviderConfig { type: ProviderType.NETWORK; - refresh_interval: number; + /** + * How often this provider refreshes in milliseconds. + */ + refreshInterval?: number; } -export interface NetworkVariables { +export interface NetworkProvider { defaultInterface: NetworkInterface | null; defaultGateway: NetworkGateway | null; interfaces: NetworkInterface[]; @@ -68,7 +71,7 @@ export async function createNetworkProvider( ) { const networkVariables = await createProviderListener< NetworkProviderConfig, - NetworkVariables + NetworkProvider >(config, owner); return { diff --git a/packages/client-api/src/providers/util/create-util-provider.ts b/packages/client-api/src/providers/util/create-util-provider.ts index 6ea795b0..f2ab36a8 100644 --- a/packages/client-api/src/providers/util/create-util-provider.ts +++ b/packages/client-api/src/providers/util/create-util-provider.ts @@ -6,6 +6,14 @@ export interface UtilProviderConfig { type: ProviderType.UTIL; } +export interface UtilProvider { + convertBytes( + bytes: number, + decimals?: number, + unitType?: DataUnit, + ): string; +} + export enum DataUnit { BITS = 'bits', SI_BYTES = 'si_bytes', @@ -15,7 +23,7 @@ export enum DataUnit { export async function createUtilProvider( _: UtilProviderConfig, __: Owner, -) { +): Promise { const bitUnits = ['b', 'Kb', 'Mb', 'Gb', 'Tb', 'Pb', 'Eb', 'Zb', 'Yb']; const byteCommonUnits = [ diff --git a/packages/client-api/src/providers/weather/create-weather-provider.ts b/packages/client-api/src/providers/weather/create-weather-provider.ts index 4c699a79..f65fafad 100644 --- a/packages/client-api/src/providers/weather/create-weather-provider.ts +++ b/packages/client-api/src/providers/weather/create-weather-provider.ts @@ -1,7 +1,7 @@ import type { Owner } from 'solid-js'; import { - type IpVariables, + type IpProvider, createIpProvider, } from '../ip/create-ip-provider'; import { WeatherStatus } from './weather-status.enum'; @@ -26,10 +26,10 @@ export interface WeatherProviderConfig { /** * How often this provider refreshes in milliseconds. */ - refresh_interval?: number; + refreshInterval?: number; } -export interface WeatherVariables { +export interface WeatherProvider { isDaytime: boolean; status: WeatherStatus; celsiusTemp: number; @@ -41,17 +41,18 @@ export async function createWeatherProvider( config: WeatherProviderConfig, owner: Owner, ) { - let ipProvider: IpVariables | null = null; + let ipProvider: IpProvider | null = null; - const mergedConfig = { + const mergedConfig: WeatherProviderConfig = { ...config, + refreshInterval: config.refreshInterval ?? 60 * 60 * 1000, longitude: config.longitude ?? (await getIpProvider()).approxLongitude, latitude: config.latitude ?? (await getIpProvider()).approxLatitude, }; const weatherVariables = await createProviderListener< WeatherProviderConfig, - WeatherVariables + WeatherProvider >(mergedConfig, owner); async function getIpProvider() { @@ -60,7 +61,7 @@ export async function createWeatherProvider( (ipProvider = await createIpProvider( { type: ProviderType.IP, - refresh_interval: 60 * 60 * 1000, + refreshInterval: 60 * 60 * 1000, }, owner, )) From ef0b375ca4a22e882d6a809bd0015fc24eb94a49 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Fri, 23 Aug 2024 02:42:11 +0800 Subject: [PATCH 013/138] feat: simplify ts typing of `createProvider` --- .../battery/create-battery-provider.ts | 3 +- .../src/providers/cpu/create-cpu-provider.ts | 3 +- .../src/providers/create-provider-listener.ts | 2 +- .../src/providers/create-provider.ts | 184 +++++------------- .../providers/date/create-date-provider.ts | 4 +- .../glazewm/create-glazewm-provider.ts | 3 +- .../providers/host/create-host-provider.ts | 3 +- .../src/providers/ip/create-ip-provider.ts | 3 +- .../komorebi/create-komorebi-provider.ts | 3 +- .../memory/create-memory-provider.ts | 3 +- .../network/create-network-provider.ts | 3 +- .../src/providers/provider-config.model.ts | 24 --- .../src/providers/provider-type.model.ts | 17 -- .../providers/util/create-util-provider.ts | 4 +- .../weather/create-weather-provider.ts | 5 +- .../client-api/src/zebar-context.model.ts | 2 +- 16 files changed, 65 insertions(+), 201 deletions(-) delete mode 100644 packages/client-api/src/providers/provider-config.model.ts delete mode 100644 packages/client-api/src/providers/provider-type.model.ts diff --git a/packages/client-api/src/providers/battery/create-battery-provider.ts b/packages/client-api/src/providers/battery/create-battery-provider.ts index 17e0fd27..263c6eaf 100644 --- a/packages/client-api/src/providers/battery/create-battery-provider.ts +++ b/packages/client-api/src/providers/battery/create-battery-provider.ts @@ -1,10 +1,9 @@ import type { Owner } from 'solid-js'; import { createProviderListener } from '../create-provider-listener'; -import type { ProviderType } from '../provider-type.model'; export interface BatteryProviderConfig { - type: ProviderType.BATTERY; + type: 'battery'; /** * How often this provider refreshes in milliseconds. diff --git a/packages/client-api/src/providers/cpu/create-cpu-provider.ts b/packages/client-api/src/providers/cpu/create-cpu-provider.ts index ee4b45ed..a36a34c9 100644 --- a/packages/client-api/src/providers/cpu/create-cpu-provider.ts +++ b/packages/client-api/src/providers/cpu/create-cpu-provider.ts @@ -1,10 +1,9 @@ import type { Owner } from 'solid-js'; import { createProviderListener } from '../create-provider-listener'; -import { ProviderType } from '../provider-type.model'; export interface CpuProviderConfig { - type: ProviderType.CPU; + type: 'cpu'; /** * How often this provider refreshes in milliseconds. diff --git a/packages/client-api/src/providers/create-provider-listener.ts b/packages/client-api/src/providers/create-provider-listener.ts index 2bb14369..914cdfd6 100644 --- a/packages/client-api/src/providers/create-provider-listener.ts +++ b/packages/client-api/src/providers/create-provider-listener.ts @@ -13,7 +13,7 @@ import { unlistenProvider, } from '~/desktop'; import { simpleHash } from '~/utils'; -import type { ProviderConfig } from './provider-config.model'; +import type { ProviderConfig } from './create-provider'; /** * Utility for listening to a provider of a given config type. diff --git a/packages/client-api/src/providers/create-provider.ts b/packages/client-api/src/providers/create-provider.ts index cda151a9..05a91080 100644 --- a/packages/client-api/src/providers/create-provider.ts +++ b/packages/client-api/src/providers/create-provider.ts @@ -2,178 +2,96 @@ import type { Owner } from 'solid-js'; import { createBatteryProvider, - type BatteryProvider, + type BatteryProviderConfig, } from './battery/create-battery-provider'; import { createCpuProvider, - type CpuProvider, + type CpuProviderConfig, } from './cpu/create-cpu-provider'; import { createDateProvider, - type DateProvider, + type DateProviderConfig, } from './date/create-date-provider'; import { createGlazeWmProvider, - type GlazeWmProvider, + type GlazeWmProviderConfig, } from './glazewm/create-glazewm-provider'; import { createHostProvider, - type HostProvider, + type HostProviderConfig, } from './host/create-host-provider'; import { createIpProvider, - type IpProvider, + type IpProviderConfig, } from './ip/create-ip-provider'; import { createKomorebiProvider, - type KomorebiProvider, + type KomorebiProviderConfig, } from './komorebi/create-komorebi-provider'; import { createMemoryProvider, - type MemoryProvider, + type MemoryProviderConfig, } from './memory/create-memory-provider'; import { createNetworkProvider, - type NetworkProvider, + type NetworkProviderConfig, } from './network/create-network-provider'; import { createUtilProvider, - type UtilProvider, + type UtilProviderConfig, } from './util/create-util-provider'; import { createWeatherProvider, - type WeatherProvider, + type WeatherProviderConfig, } from './weather/create-weather-provider'; -import type { ProviderConfig } from './provider-config.model'; -import { ProviderType } from './provider-type.model'; -// type ProviderTypeToProvider = { -// [ProviderType.BATTERY]: BatteryProvider; -// [ProviderType.CPU]: CpuProvider; -// [ProviderType.DATE]: DateProvider; -// [ProviderType.GLAZEWM]: GlazeWmProvider; -// [ProviderType.HOST]: HostProvider; -// [ProviderType.IP]: IpProvider; -// [ProviderType.KOMOREBI]: KomorebiProvider; -// [ProviderType.MEMORY]: MemoryProvider; -// [ProviderType.NETWORK]: NetworkProvider; -// [ProviderType.UTIL]: UtilProvider; -// [ProviderType.WEATHER]: WeatherProvider; -// }; - -async function aa() { - const xx = await createProvider({ type: ProviderType.UTIL }, {} as any); -} - -// const createProviderMap = { -// [ProviderType.BATTERY]: createBatteryProvider, -// [ProviderType.CPU]: createCpuProvider, -// [ProviderType.DATE]: createDateProvider, -// [ProviderType.GLAZEWM]: createGlazeWmProvider, -// [ProviderType.HOST]: createHostProvider, -// [ProviderType.IP]: createIpProvider, -// [ProviderType.KOMOREBI]: createKomorebiProvider, -// [ProviderType.MEMORY]: createMemoryProvider, -// [ProviderType.NETWORK]: createNetworkProvider, -// [ProviderType.UTIL]: createUtilProvider, -// [ProviderType.WEATHER]: createWeatherProvider, -// } as const; - -// type ProviderCreator = ( -// config: T, -// owner: Owner, -// ) => Promise; - -// type ProviderCreators = { -// [T in ProviderType]: ProviderCreator< -// Extract -// >; -// }; - -// const providerCreators: ProviderCreators = { -// [ProviderType.BATTERY]: createBatteryProvider, -// [ProviderType.CPU]: createCpuProvider, -// [ProviderType.DATE]: createDateProvider, -// [ProviderType.GLAZEWM]: createGlazeWmProvider, -// [ProviderType.HOST]: createHostProvider, -// [ProviderType.IP]: createIpProvider, -// [ProviderType.KOMOREBI]: createKomorebiProvider, -// [ProviderType.MEMORY]: createMemoryProvider, -// [ProviderType.NETWORK]: createNetworkProvider, -// [ProviderType.UTIL]: createUtilProvider, -// [ProviderType.WEATHER]: createWeatherProvider, -// }; - -// export async function createProvider( -// config: T, -// owner: Owner, -// ): Promise> { -// const creator = providerCreators[config.type] as ProviderCreator; - -// if (!creator) { -// throw new Error('Not a supported provider type.'); -// } - -// return creator(config, owner); -// } - -// ): Promise { -// ) { -// const createProviderMap = { -// [ProviderType.BATTERY]: createBatteryProvider, -// [ProviderType.CPU]: createCpuProvider, -// [ProviderType.DATE]: createDateProvider, -// [ProviderType.GLAZEWM]: createGlazeWmProvider, -// [ProviderType.HOST]: createHostProvider, -// [ProviderType.IP]: createIpProvider, -// [ProviderType.KOMOREBI]: createKomorebiProvider, -// [ProviderType.MEMORY]: createMemoryProvider, -// [ProviderType.NETWORK]: createNetworkProvider, -// [ProviderType.UTIL]: createUtilProvider, -// [ProviderType.WEATHER]: createWeatherProvider, -// } as const; - -// return createProviderMap[config.type](config as any, owner); -// // const providerFn = createProviderMap[config.type] as ( -// // config: any, -// // owner: Owner, -// // ) => Promise; - -// // if (!providerFn) { -// // throw new Error('Not a supported provider type.'); -// // } - -// // return providerFn(config, owner); -// } - -const providerCreators = { - [ProviderType.BATTERY]: createBatteryProvider, - [ProviderType.CPU]: createCpuProvider, - [ProviderType.DATE]: createDateProvider, - [ProviderType.GLAZEWM]: createGlazeWmProvider, - [ProviderType.HOST]: createHostProvider, - [ProviderType.IP]: createIpProvider, - [ProviderType.KOMOREBI]: createKomorebiProvider, - [ProviderType.MEMORY]: createMemoryProvider, - [ProviderType.NETWORK]: createNetworkProvider, - [ProviderType.UTIL]: createUtilProvider, - [ProviderType.WEATHER]: createWeatherProvider, +export type ProviderConfig = + | BatteryProviderConfig + | CpuProviderConfig + | DateProviderConfig + | GlazeWmProviderConfig + | HostProviderConfig + | IpProviderConfig + | KomorebiProviderConfig + | MemoryProviderConfig + | NetworkProviderConfig + | UtilProviderConfig + | WeatherProviderConfig; + +export type ProviderType = ProviderConfig['type']; + +const createProviderMap = { + battery: createBatteryProvider, + cpu: createCpuProvider, + date: createDateProvider, + glazewm: createGlazeWmProvider, + host: createHostProvider, + ip: createIpProvider, + komorebi: createKomorebiProvider, + memory: createMemoryProvider, + network: createNetworkProvider, + util: createUtilProvider, + weather: createWeatherProvider, } as const; -type ProviderCreatorMap = typeof providerCreators; -type ProviderConfigType = ProviderConfig & { - type: keyof ProviderCreatorMap; -}; +type ProviderMap = typeof createProviderMap; + +/** + * Utility type to get the return value of a provider. + * + * @example `Provider<'battery'> = BatteryProvider` + */ +type Provider = ReturnType; -export async function createProvider( +export async function createProvider( config: T, owner: Owner, -): Promise> { - const creator = providerCreators[config.type]; +): Promise> { + const providerFn = createProviderMap[config.type]; - if (!creator) { + if (!providerFn) { throw new Error('Not a supported provider type.'); } - return creator(config as any, owner) as any; + return providerFn(config as any, owner) as Provider; } diff --git a/packages/client-api/src/providers/date/create-date-provider.ts b/packages/client-api/src/providers/date/create-date-provider.ts index 36f3e91c..f7f7452e 100644 --- a/packages/client-api/src/providers/date/create-date-provider.ts +++ b/packages/client-api/src/providers/date/create-date-provider.ts @@ -2,10 +2,8 @@ import { DateTime } from 'luxon'; import { type Owner, onCleanup, runWithOwner } from 'solid-js'; import { createStore } from 'solid-js/store'; -import type { ProviderType } from '../provider-type.model'; - export interface DateProviderConfig { - type: ProviderType.DATE; + type: 'date'; /** * How often this provider refreshes in milliseconds. diff --git a/packages/client-api/src/providers/glazewm/create-glazewm-provider.ts b/packages/client-api/src/providers/glazewm/create-glazewm-provider.ts index f2a7e249..8da41ceb 100644 --- a/packages/client-api/src/providers/glazewm/create-glazewm-provider.ts +++ b/packages/client-api/src/providers/glazewm/create-glazewm-provider.ts @@ -19,10 +19,9 @@ import { createStore } from 'solid-js/store'; import { getMonitors } from '~/desktop'; import { getCoordinateDistance } from '~/utils'; -import { ProviderType } from '../provider-type.model'; export interface GlazeWmProviderConfig { - type: ProviderType.GLAZEWM; + type: 'glazewm'; } export interface GlazeWmProvider { diff --git a/packages/client-api/src/providers/host/create-host-provider.ts b/packages/client-api/src/providers/host/create-host-provider.ts index 581f2595..5fd01203 100644 --- a/packages/client-api/src/providers/host/create-host-provider.ts +++ b/packages/client-api/src/providers/host/create-host-provider.ts @@ -1,10 +1,9 @@ import type { Owner } from 'solid-js'; import { createProviderListener } from '../create-provider-listener'; -import type { ProviderType } from '../provider-type.model'; export interface HostProviderConfig { - type: ProviderType.HOST; + type: 'host'; /** * How often this provider refreshes in milliseconds. diff --git a/packages/client-api/src/providers/ip/create-ip-provider.ts b/packages/client-api/src/providers/ip/create-ip-provider.ts index 18e67755..52b51d75 100644 --- a/packages/client-api/src/providers/ip/create-ip-provider.ts +++ b/packages/client-api/src/providers/ip/create-ip-provider.ts @@ -1,10 +1,9 @@ import type { Owner } from 'solid-js'; import { createProviderListener } from '../create-provider-listener'; -import type { ProviderType } from '../provider-type.model'; export interface IpProviderConfig { - type: ProviderType.IP; + type: 'ip'; /** * How often this provider refreshes in milliseconds. diff --git a/packages/client-api/src/providers/komorebi/create-komorebi-provider.ts b/packages/client-api/src/providers/komorebi/create-komorebi-provider.ts index 98b1111a..44ac1c22 100644 --- a/packages/client-api/src/providers/komorebi/create-komorebi-provider.ts +++ b/packages/client-api/src/providers/komorebi/create-komorebi-provider.ts @@ -4,10 +4,9 @@ import { createStore } from 'solid-js/store'; import { createProviderListener } from '../create-provider-listener'; import { getMonitors } from '~/desktop'; import { getCoordinateDistance } from '~/utils'; -import { ProviderType } from '../provider-type.model'; export interface KomorebiProviderConfig { - type: ProviderType.KOMOREBI; + type: 'komorebi'; } export interface KomorebiProvider { diff --git a/packages/client-api/src/providers/memory/create-memory-provider.ts b/packages/client-api/src/providers/memory/create-memory-provider.ts index 7fbb593c..97e6b1ba 100644 --- a/packages/client-api/src/providers/memory/create-memory-provider.ts +++ b/packages/client-api/src/providers/memory/create-memory-provider.ts @@ -1,10 +1,9 @@ import type { Owner } from 'solid-js'; import { createProviderListener } from '../create-provider-listener'; -import type { ProviderType } from '../provider-type.model'; export interface MemoryProviderConfig { - type: ProviderType.MEMORY; + type: 'memory'; /** * How often this provider refreshes in milliseconds. diff --git a/packages/client-api/src/providers/network/create-network-provider.ts b/packages/client-api/src/providers/network/create-network-provider.ts index 94a594fa..12c17e77 100644 --- a/packages/client-api/src/providers/network/create-network-provider.ts +++ b/packages/client-api/src/providers/network/create-network-provider.ts @@ -1,10 +1,9 @@ import type { Owner } from 'solid-js'; import { createProviderListener } from '../create-provider-listener'; -import type { ProviderType } from '../provider-type.model'; export interface NetworkProviderConfig { - type: ProviderType.NETWORK; + type: 'network'; /** * How often this provider refreshes in milliseconds. diff --git a/packages/client-api/src/providers/provider-config.model.ts b/packages/client-api/src/providers/provider-config.model.ts deleted file mode 100644 index 6ff2acc6..00000000 --- a/packages/client-api/src/providers/provider-config.model.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { BatteryProviderConfig } from './battery/create-battery-provider'; -import type { CpuProviderConfig } from './cpu/create-cpu-provider'; -import type { DateProviderConfig } from './date/create-date-provider'; -import type { GlazeWmProviderConfig } from './glazewm/create-glazewm-provider'; -import type { HostProviderConfig } from './host/create-host-provider'; -import type { IpProviderConfig } from './ip/create-ip-provider'; -import type { KomorebiProviderConfig } from './komorebi/create-komorebi-provider'; -import type { MemoryProviderConfig } from './memory/create-memory-provider'; -import type { NetworkProviderConfig } from './network/create-network-provider'; -import type { UtilProviderConfig } from './util/create-util-provider'; -import type { WeatherProviderConfig } from './weather/create-weather-provider'; - -export type ProviderConfig = - | BatteryProviderConfig - | CpuProviderConfig - | DateProviderConfig - | GlazeWmProviderConfig - | HostProviderConfig - | IpProviderConfig - | KomorebiProviderConfig - | MemoryProviderConfig - | NetworkProviderConfig - | UtilProviderConfig - | WeatherProviderConfig; diff --git a/packages/client-api/src/providers/provider-type.model.ts b/packages/client-api/src/providers/provider-type.model.ts deleted file mode 100644 index 4cefe5bc..00000000 --- a/packages/client-api/src/providers/provider-type.model.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { z } from 'zod'; - -export enum ProviderType { - BATTERY = 'battery', - CPU = 'cpu', - DATE = 'date', - GLAZEWM = 'glazewm', - HOST = 'host', - IP = 'ip', - KOMOREBI = 'komorebi', - MEMORY = 'memory', - NETWORK = 'network', - UTIL = 'util', - WEATHER = 'weather', -} - -export const ProviderTypeSchema = z.nativeEnum(ProviderType); diff --git a/packages/client-api/src/providers/util/create-util-provider.ts b/packages/client-api/src/providers/util/create-util-provider.ts index f2ab36a8..56b01cf4 100644 --- a/packages/client-api/src/providers/util/create-util-provider.ts +++ b/packages/client-api/src/providers/util/create-util-provider.ts @@ -1,9 +1,7 @@ import type { Owner } from 'solid-js'; -import type { ProviderType } from '../provider-type.model'; - export interface UtilProviderConfig { - type: ProviderType.UTIL; + type: 'util'; } export interface UtilProvider { diff --git a/packages/client-api/src/providers/weather/create-weather-provider.ts b/packages/client-api/src/providers/weather/create-weather-provider.ts index f65fafad..f52b57a2 100644 --- a/packages/client-api/src/providers/weather/create-weather-provider.ts +++ b/packages/client-api/src/providers/weather/create-weather-provider.ts @@ -6,10 +6,9 @@ import { } from '../ip/create-ip-provider'; import { WeatherStatus } from './weather-status.enum'; import { createProviderListener } from '../create-provider-listener'; -import { ProviderType } from '../provider-type.model'; export interface WeatherProviderConfig { - type: ProviderType.WEATHER; + type: 'weather'; /** * Latitude to retrieve weather for. If not provided, latitude is instead @@ -60,7 +59,7 @@ export async function createWeatherProvider( ipProvider ?? (ipProvider = await createIpProvider( { - type: ProviderType.IP, + type: 'ip', refreshInterval: 60 * 60 * 1000, }, owner, diff --git a/packages/client-api/src/zebar-context.model.ts b/packages/client-api/src/zebar-context.model.ts index a0c497e2..69959249 100644 --- a/packages/client-api/src/zebar-context.model.ts +++ b/packages/client-api/src/zebar-context.model.ts @@ -1,7 +1,7 @@ import { Window as TauriWindow } from '@tauri-apps/api/window'; import type { WindowConfig, WindowZOrder } from '~/user-config'; -import type { ProviderConfig } from './providers/provider-config.model'; +import type { ProviderConfig } from './providers'; export interface ZebarContext { /** From 46107a1dbded810f4d0cfb28fe5e992c61d78167 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Fri, 23 Aug 2024 04:43:09 +0800 Subject: [PATCH 014/138] feat: expose `createProvider` function on context --- packages/client-api/src/init.ts | 14 +++++--------- .../client-api/src/providers/create-provider.ts | 10 ++++++---- packages/client-api/src/zebar-context.model.ts | 17 +++++++---------- 3 files changed, 18 insertions(+), 23 deletions(-) diff --git a/packages/client-api/src/init.ts b/packages/client-api/src/init.ts index 84cb8e02..de2aff5b 100644 --- a/packages/client-api/src/init.ts +++ b/packages/client-api/src/init.ts @@ -4,7 +4,7 @@ import { createRoot, getOwner, runWithOwner } from 'solid-js'; import { getInitialState, openWindow, showErrorDialog } from './desktop'; import { createLogger } from '~/utils'; import type { ZebarContext } from './zebar-context.model'; -import { createStore } from 'solid-js/store'; +import { createProvider } from './providers'; const logger = createLogger('init-window'); @@ -22,25 +22,21 @@ export async function initAsync(): Promise { return withReactiveContext(async () => { try { const currentWindow = getCurrentWindow(); + const initialState = window.__ZEBAR_INITIAL_STATE ?? (await getInitialState(currentWindow.label)); - const providers = createStore([]); - await currentWindow.show(); - function createProvider() { - // TODO - } - // @ts-ignore - TODO return { // @ts-ignore - TODO config: initialState.config, - providers, openWindow, - createProvider: () => {}, + createProvider: config => { + return createProvider(config, getOwner()!); + }, currentWindow: {}, allWindows: [], currentMonitor: {}, diff --git a/packages/client-api/src/providers/create-provider.ts b/packages/client-api/src/providers/create-provider.ts index 05a91080..5a756177 100644 --- a/packages/client-api/src/providers/create-provider.ts +++ b/packages/client-api/src/providers/create-provider.ts @@ -81,17 +81,19 @@ type ProviderMap = typeof createProviderMap; * * @example `Provider<'battery'> = BatteryProvider` */ -type Provider = ReturnType; +export type ProviderOutput = ReturnType< + ProviderMap[T] +>; -export async function createProvider( +export function createProvider( config: T, owner: Owner, -): Promise> { +): ProviderOutput { const providerFn = createProviderMap[config.type]; if (!providerFn) { throw new Error('Not a supported provider type.'); } - return providerFn(config as any, owner) as Provider; + return providerFn(config as any, owner) as ProviderOutput; } diff --git a/packages/client-api/src/zebar-context.model.ts b/packages/client-api/src/zebar-context.model.ts index 69959249..61992c6f 100644 --- a/packages/client-api/src/zebar-context.model.ts +++ b/packages/client-api/src/zebar-context.model.ts @@ -1,19 +1,14 @@ import { Window as TauriWindow } from '@tauri-apps/api/window'; import type { WindowConfig, WindowZOrder } from '~/user-config'; -import type { ProviderConfig } from './providers'; +import type { ProviderConfig, ProviderOutput } from './providers'; -export interface ZebarContext { +export interface ZebarContext { /** * Parsed window config. */ config: WindowConfig; - /** - * Map of this element's providers and their variables. - */ - providers: TProviders; - currentWindow: ZebarWindow; allWindows: ZebarWindow; @@ -25,10 +20,10 @@ export interface ZebarContext { /** * Opens a new window by a relative path to its config file. */ - openWindow: ( + openWindow( configPath: string, args?: Record, - ) => Promise; + ): Promise; /** * Initializes a provider. @@ -36,7 +31,9 @@ export interface ZebarContext { * If an existing provider with the same config exists, that provider * instance will be re-used. */ - createProvider: (providerConfig: ProviderConfig) => Promise; + createProvider( + providerConfig: T, + ): ProviderOutput; } export interface ZebarWindow { From 624617fedf9b125cf9f36e7442d3c20e89cd06ec Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Fri, 23 Aug 2024 15:02:28 +0800 Subject: [PATCH 015/138] feat: add init option to include default css --- packages/client-api/src/index.ts | 2 +- packages/client-api/src/init.ts | 26 ++++++++++++++----- packages/client-api/src/shims.d.ts | 5 ++++ .../src/zebar.css} | 23 ++++++++++++++++ packages/client-api/tsup.config.ts | 2 ++ packages/client/index.html | 1 + packages/client/src/index.css | 26 ------------------- packages/client/src/index.tsx | 5 ++-- 8 files changed, 55 insertions(+), 35 deletions(-) rename packages/{client/src/normalize.css => client-api/src/zebar.css} (95%) delete mode 100644 packages/client/src/index.css diff --git a/packages/client-api/src/index.ts b/packages/client-api/src/index.ts index 429d5d7c..2228ee93 100644 --- a/packages/client-api/src/index.ts +++ b/packages/client-api/src/index.ts @@ -1,2 +1,2 @@ export { type WindowConfig } from './user-config'; -export { init, initAsync } from './init'; +export { init } from './init'; diff --git a/packages/client-api/src/init.ts b/packages/client-api/src/init.ts index de2aff5b..95524890 100644 --- a/packages/client-api/src/init.ts +++ b/packages/client-api/src/init.ts @@ -8,17 +8,26 @@ import { createProvider } from './providers'; const logger = createLogger('init-window'); -/** - * Handles initialization. - */ -export function init(callback: (context: ZebarContext) => void) { - initAsync().then(callback); +export interface ZebarInitOptions { + /** + * Whether to add basic default CSS in the window. + * + * Includes: + * - Setting box-sizing to border-box + * - Disabling overscroll + * - [normalize.css](https://github.com/necolas/normalize.css) + * + * Defaults to `true`. + */ + includeDefaultCss?: boolean; } /** * Handles initialization. */ -export async function initAsync(): Promise { +export async function init( + options?: ZebarInitOptions, +): Promise { return withReactiveContext(async () => { try { const currentWindow = getCurrentWindow(); @@ -27,6 +36,11 @@ export async function initAsync(): Promise { window.__ZEBAR_INITIAL_STATE ?? (await getInitialState(currentWindow.label)); + // Load default CSS unless explicitly disabled. + if (options?.includeDefaultCss !== false) { + import('./zebar.css'); + } + await currentWindow.show(); // @ts-ignore - TODO diff --git a/packages/client-api/src/shims.d.ts b/packages/client-api/src/shims.d.ts index 20969028..ed83ef9b 100644 --- a/packages/client-api/src/shims.d.ts +++ b/packages/client-api/src/shims.d.ts @@ -2,3 +2,8 @@ interface Window { __TAURI__: any; __ZEBAR_INITIAL_STATE: import('./desktop').OpenWindowArgs; } + +declare module '*.css' { + const src: string; + export default src; +} diff --git a/packages/client/src/normalize.css b/packages/client-api/src/zebar.css similarity index 95% rename from packages/client/src/normalize.css rename to packages/client-api/src/zebar.css index 2768db43..5cb1d3c4 100644 --- a/packages/client/src/normalize.css +++ b/packages/client-api/src/zebar.css @@ -1,3 +1,26 @@ +html { + box-sizing: border-box; +} + +/** + * Set default `box-sizing` for all elements to border-box. + */ +*, +*:before, +*:after { + box-sizing: inherit; +} + +/** + * Disable overscroll. + */ +body { + overflow: hidden; + overscroll-behavior-y: none; + overscroll-behavior-x: none; +} + + /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ /* Document diff --git a/packages/client-api/tsup.config.ts b/packages/client-api/tsup.config.ts index 88706395..f969fc41 100644 --- a/packages/client-api/tsup.config.ts +++ b/packages/client-api/tsup.config.ts @@ -25,6 +25,8 @@ const CI = export default defineConfig(config => { const watching = !!config.watch; + config.injectStyle = true; + const parsed_options = preset.parsePresetOptions( preset_options, watching, diff --git a/packages/client/index.html b/packages/client/index.html index 495ff786..b12521f4 100644 --- a/packages/client/index.html +++ b/packages/client/index.html @@ -1,5 +1,6 @@ + + + diff --git a/packages/desktop/resources/starter/komorebi.json b/packages/desktop/resources/starter/komorebi.json new file mode 100644 index 00000000..d4656dd3 --- /dev/null +++ b/packages/desktop/resources/starter/komorebi.json @@ -0,0 +1,19 @@ +{ + "html_path": "./glazewm.html", + "launch_options": { + "z_order": "normal", + "shown_in_taskbar": false, + "resizable": false, + "transparent": true, + "placement": [ + { + "monitor": 0, + "anchor": "top_left", + "offset_x": "20px", + "offset_y": "20px", + "width": "100%", + "height": "30px" + } + ] + } +} diff --git a/packages/desktop/resources/starter/komorebi.yaml b/packages/desktop/resources/starter/komorebi.yaml new file mode 100644 index 00000000..a6074d1f --- /dev/null +++ b/packages/desktop/resources/starter/komorebi.yaml @@ -0,0 +1,41 @@ +# Entry point HTML file. +html_path: './komorebi.html' + +# Default options for when the window is opened. +# These can be overridden when opening the window programmatically. +launch_options: + # Whether to show the window above/below all others. + # Allowed values: 'always_on_top', 'always_on_bottom', 'normal'. + z_order: 'normal' + + # Whether the window should be shown in the taskbar. + shown_in_taskbar: false + + # Whether the window should have resize handles. + resizable: false + + # Whether the window frame should be transparent. + transparent: true + + # Where to place the window. + placement: + # The monitor index to place the window on. + # Allowed values: 0, 1, 2, 3, etc. or 'all'. + - monitor: 0 + + # Anchor-point of the window. + # Allowed values: either 'center' or combinations of 'top', 'center', + # 'bottom' and 'left', 'center', 'right'. + anchor: 'top left' + + # Offset from the anchor-point. + offset_x: '20px' + + # Offset from the anchor-point. + offset_y: '20px' + + # Width of the window in % or physical pixels. + width: '100%' + + # Height of the window in % or physical pixels. + height: '30px' diff --git a/packages/desktop/src/main.rs b/packages/desktop/src/main.rs index 2e235697..ad85ad23 100644 --- a/packages/desktop/src/main.rs +++ b/packages/desktop/src/main.rs @@ -1,10 +1,11 @@ #![feature(async_closure)] -use std::{collections::HashMap, env}; +use std::{collections::HashMap, env, sync::Arc}; use clap::Parser; use tauri::{Manager, State, Window}; use tracing::level_filters::LevelFilter; use tracing_subscriber::EnvFilter; +use user_config::Config; use crate::{ cli::{Cli, CliCommand, OpenWindowArgs, OutputMonitorsArgs}, @@ -27,7 +28,9 @@ mod window_factory; async fn get_open_window_args( window_label: String, window_factory: State<'_, WindowFactory>, + config: State<'_, Config>, ) -> anyhow::Result, String> { + println!("{:?}", config.config_dir); Ok(window_factory.state_by_window_label(window_label).await) } @@ -134,6 +137,11 @@ fn start_app(cli: Cli) { tauri::Builder::default() .setup(|app| { + let mut config = Config::new(app.handle())?; + config.read()?; + println!("{:?}", config); + app.manage(config); + // If this is not the first instance of the app, this will // emit within the original instance and exit // immediately. @@ -148,8 +156,6 @@ fn start_app(cli: Cli) { }, ))?; - let config = user_config::read_file(None, app.handle())?; - // Prevent windows from showing up in the dock on MacOS. #[cfg(target_os = "macos")] app.set_activation_policy(tauri::ActivationPolicy::Accessory); diff --git a/packages/desktop/src/sys_tray.rs b/packages/desktop/src/sys_tray.rs index 36612ab7..b8277d24 100644 --- a/packages/desktop/src/sys_tray.rs +++ b/packages/desktop/src/sys_tray.rs @@ -5,8 +5,6 @@ use tauri::{ }; use tracing::{error, info}; -use crate::user_config::open_config_dir; - pub fn setup_sys_tray(app: &mut tauri::App) -> anyhow::Result { let icon_image = app .default_window_icon() @@ -25,9 +23,9 @@ pub fn setup_sys_tray(app: &mut tauri::App) -> anyhow::Result { .on_menu_event(move |app, event| match event.id().as_ref() { "show_config_folder" => { info!("Opening config folder from system tray."); - if let Err(err) = open_config_dir(app) { - error!("Failed to open config folder: {}", err); - } + // if let Err(err) = open_config_dir(app) { + // error!("Failed to open config folder: {}", err); + // } } "exit" => { info!("Exiting through system tray."); diff --git a/packages/desktop/src/user_config.rs b/packages/desktop/src/user_config.rs index f2b3d2d0..15a51556 100644 --- a/packages/desktop/src/user_config.rs +++ b/packages/desktop/src/user_config.rs @@ -1,4 +1,7 @@ -use std::{fs, path::PathBuf}; +use std::{ + fs::{self, DirEntry}, + path::PathBuf, +}; use anyhow::Context; use serde::{Deserialize, Serialize}; @@ -77,102 +80,174 @@ pub enum WindowAnchor { BottomRight, } -/// Reads the config file at `~/.glzr/zebar/config.yaml`. -pub fn read_file( - config_path_override: Option<&str>, - app_handle: &AppHandle, -) -> anyhow::Result { - let default_config_path = app_handle - .path() - .resolve(".glzr/zebar/config.yaml", BaseDirectory::Home) - .context("Unable to get home directory.")?; - - let config_path = match config_path_override { - Some(val) => PathBuf::from(val), - None => default_config_path, - }; - - // Create new config file from sample if it doesn't exist. - if !config_path.exists() { - create_from_sample(&config_path, app_handle)?; - } - - let config_str = fs::read_to_string(&config_path) - .context("Unable to read config file.")?; +#[derive(Debug)] +pub struct Config { + /// Handle to the Tauri application. + // app_handle: AppHandle, - // TODO: Improve error formatting of serde_yaml errors. Something - // similar to https://github.com/AlexanderThaller/format_serde_error - let config_value = serde_yaml::from_str(&config_str)?; + /// Directory where config files are stored. + pub config_dir: PathBuf, - Ok(config_value) + /// Config entries. + pub config_entries: Vec, } -/// Initialize config at the given path from the sample config resource. -fn create_from_sample( - config_path: &PathBuf, - app_handle: &AppHandle, -) -> anyhow::Result<()> { - let sample_path = app_handle - .path() - .resolve("resources/sample-config.yaml", BaseDirectory::Resource) - .context("Unable to resolve sample config resource.")?; - - let sample_script = app_handle - .path() - .resolve("resources/script.js", BaseDirectory::Resource) - .context("Unable to resolve sample script resource.")?; - - let dest_dir = - config_path.parent().context("Invalid config directory.")?; - - // Create the destination directory. - std::fs::create_dir_all(&dest_dir).with_context(|| { - format!("Unable to create directory {}.", &config_path.display()) - })?; - - // Copy over sample config. - let config_path = dest_dir.join("config.yaml"); - fs::copy(&sample_path, &config_path).with_context(|| { - format!("Unable to write to {}.", config_path.display()) - })?; - - // Copy over sample script. - let script_path = dest_dir.join("script.js"); - fs::copy(&sample_script, &script_path).with_context(|| { - format!("Unable to write to {}.", script_path.display()) - })?; - - Ok(()) +#[derive(Debug)] +pub struct ConfigEntry { + path: PathBuf, + config: WindowConfig, } -pub fn open_config_dir(app_handle: &AppHandle) -> anyhow::Result<()> { - let dir_path = app_handle - .path() - .resolve(".glzr/zebar", BaseDirectory::Home) - .context("Unable to get home directory.")? - .canonicalize()?; - - #[cfg(target_os = "windows")] - { - std::process::Command::new("explorer") - .arg(dir_path) - .spawn()?; +impl Config { + /// Creates a new `Config` instance. + pub fn new(app_handle: &AppHandle) -> anyhow::Result { + let config_dir = app_handle + .path() + .resolve(".glzr/zebar", BaseDirectory::Home) + .context("Unable to get home directory.")?; + + Ok(Self { + config_dir, + config_entries: Vec::new(), + }) + } + + /// Reads the config files within the config directory. + pub fn read(&mut self) -> anyhow::Result<()> { + // let mut configs = Vec::new(); + // Self::traverse_directory(self.config_dir, &mut configs)?; + self.config_entries = Self::aggregate_configs(&self.config_dir)?; + + Ok(()) } - #[cfg(target_os = "macos")] - { - std::process::Command::new("open") - .arg(dir_path) - .arg("-R") - .spawn()?; + fn aggregate_configs(dir: &PathBuf) -> anyhow::Result> { + let mut configs = Vec::new(); + + for entry in fs::read_dir(dir).with_context(|| { + format!("Failed to read directory: {}", dir.display()) + })? { + let entry = entry?; + let path = entry.path(); + + if path.is_dir() { + // Recursively call aggregate_configs on subdirectories + configs.extend(Self::aggregate_configs(&path)?); + } else if path.extension().and_then(|ext| ext.to_str()) + == Some("json") + { + if let Ok(config) = Self::process_file(&path) { + configs.push(ConfigEntry { + config, + path: path.clone(), + }); + } + } + } + + Ok(configs) } - #[cfg(target_os = "linux")] - { - std::process::Command::new("xdg-open") - .arg(dir_path) - .spawn()?; + fn is_json_file(entry: &DirEntry) -> anyhow::Result { + let file_type = entry.file_type()?; + Ok( + file_type.is_file() + && entry.path().extension().and_then(|ext| ext.to_str()) + == Some("json"), + ) } - Ok(()) + // fn traverse_directory( + // dir: &PathBuf, + // config_entries: &mut Vec, + // ) -> anyhow::Result<()> { + // for entry in fs::read_dir(dir)? { + // let entry = entry?; + // let path = entry.path(); + // if path.is_dir() { + // Self::traverse_directory(&path, config_entries)?; + // } else if Self::is_json_file(&entry)? { + // if let Ok(config_entry) = Self::process_file(entry) { + // config_entries.push(config_entry); + // } + // } + // } + // Ok(()) + // } + + fn process_file(entry: &PathBuf) -> anyhow::Result { + let content = fs::read_to_string(entry)?; + let config = serde_json::from_str(&content)?; + Ok(config) + } + + /// Initialize config at the given path from the sample config resource. + fn create_from_sample( + config_path: &PathBuf, + app_handle: &AppHandle, + ) -> anyhow::Result<()> { + let sample_path = app_handle + .path() + .resolve("resources/sample-config.yaml", BaseDirectory::Resource) + .context("Unable to resolve sample config resource.")?; + + let sample_script = app_handle + .path() + .resolve("resources/script.js", BaseDirectory::Resource) + .context("Unable to resolve sample script resource.")?; + + let dest_dir = + config_path.parent().context("Invalid config directory.")?; + + // Create the destination directory. + std::fs::create_dir_all(&dest_dir).with_context(|| { + format!("Unable to create directory {}.", &config_path.display()) + })?; + + // Copy over sample config. + let config_path = dest_dir.join("config.yaml"); + fs::copy(&sample_path, &config_path).with_context(|| { + format!("Unable to write to {}.", config_path.display()) + })?; + + // Copy over sample script. + let script_path = dest_dir.join("script.js"); + fs::copy(&sample_script, &script_path).with_context(|| { + format!("Unable to write to {}.", script_path.display()) + })?; + + Ok(()) + } + + pub fn open_config_dir(app_handle: &AppHandle) -> anyhow::Result<()> { + let dir_path = app_handle + .path() + .resolve(".glzr/zebar", BaseDirectory::Home) + .context("Unable to get home directory.")? + .canonicalize()?; + + #[cfg(target_os = "windows")] + { + std::process::Command::new("explorer") + .arg(dir_path) + .spawn()?; + } + + #[cfg(target_os = "macos")] + { + std::process::Command::new("open") + .arg(dir_path) + .arg("-R") + .spawn()?; + } + + #[cfg(target_os = "linux")] + { + std::process::Command::new("xdg-open") + .arg(dir_path) + .spawn()?; + } + + Ok(()) + } } diff --git a/packages/desktop/src/window_factory.rs b/packages/desktop/src/window_factory.rs index f5db9bcf..ae8f997a 100644 --- a/packages/desktop/src/window_factory.rs +++ b/packages/desktop/src/window_factory.rs @@ -15,8 +15,15 @@ use crate::util::WindowExt; /// Manages the creation of Zebar windows. pub struct WindowFactory { + /// Handle to the Tauri application. app_handle: AppHandle, + + /// Running total of windows created. + /// + /// Used to generate unique window labels. window_count: Arc, + + /// Map of window labels to window states. window_states: Arc>>, } From b35dc8a2dab42199b7d8dae52e5651318b5f60d1 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Fri, 23 Aug 2024 17:09:46 +0800 Subject: [PATCH 017/138] feat: change config to camelCase; reorganize `resources` dir --- Cargo.lock | 1 - packages/desktop/Cargo.toml | 1 - .../desktop/resources/config/settings.json | 4 ++ .../{ => config}/starter/glazewm.html | 0 .../{ => config}/starter/glazewm.json | 13 +++--- .../{ => config}/starter/komorebi.html | 0 .../{ => config}/starter/komorebi.json | 13 +++--- .../desktop/resources/starter/glazewm.yaml | 41 ------------------- .../desktop/resources/starter/komorebi.yaml | 41 ------------------- packages/desktop/src/user_config.rs | 9 ++-- 10 files changed, 21 insertions(+), 102 deletions(-) create mode 100644 packages/desktop/resources/config/settings.json rename packages/desktop/resources/{ => config}/starter/glazewm.html (100%) rename packages/desktop/resources/{ => config}/starter/glazewm.json (52%) rename packages/desktop/resources/{ => config}/starter/komorebi.html (100%) rename packages/desktop/resources/{ => config}/starter/komorebi.json (52%) delete mode 100644 packages/desktop/resources/starter/glazewm.yaml delete mode 100644 packages/desktop/resources/starter/komorebi.yaml diff --git a/Cargo.lock b/Cargo.lock index 83b07e7c..7e0ae722 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6514,7 +6514,6 @@ dependencies = [ "reqwest 0.11.27", "serde", "serde_json", - "serde_yaml", "starship-battery", "sysinfo", "tauri", diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 6f57deb1..668c3ab9 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -29,7 +29,6 @@ tauri-plugin-shell = "2.0.0-beta" tauri-plugin-single-instance = "2.0.0-beta" serde = { version = "1", features = ["derive"] } serde_json = "1" -serde_yaml = "0.9" starship-battery = "0.8" sysinfo = "0.30" tokio = { version = "1.33", features = ["full"] } diff --git a/packages/desktop/resources/config/settings.json b/packages/desktop/resources/config/settings.json new file mode 100644 index 00000000..1dc55776 --- /dev/null +++ b/packages/desktop/resources/config/settings.json @@ -0,0 +1,4 @@ +{ + "$schema": "TODO", + "launchOnStart": ["starter/glazewm.json"] +} diff --git a/packages/desktop/resources/starter/glazewm.html b/packages/desktop/resources/config/starter/glazewm.html similarity index 100% rename from packages/desktop/resources/starter/glazewm.html rename to packages/desktop/resources/config/starter/glazewm.html diff --git a/packages/desktop/resources/starter/glazewm.json b/packages/desktop/resources/config/starter/glazewm.json similarity index 52% rename from packages/desktop/resources/starter/glazewm.json rename to packages/desktop/resources/config/starter/glazewm.json index d4656dd3..dd8cbdf8 100644 --- a/packages/desktop/resources/starter/glazewm.json +++ b/packages/desktop/resources/config/starter/glazewm.json @@ -1,16 +1,17 @@ { - "html_path": "./glazewm.html", - "launch_options": { - "z_order": "normal", - "shown_in_taskbar": false, + "$schema": "TODO", + "htmlPath": "./glazewm.html", + "launchOptions": { + "zOrder": "normal", + "shownInTaskbar": false, "resizable": false, "transparent": true, "placement": [ { "monitor": 0, "anchor": "top_left", - "offset_x": "20px", - "offset_y": "20px", + "offsetX": "20px", + "offsetY": "20px", "width": "100%", "height": "30px" } diff --git a/packages/desktop/resources/starter/komorebi.html b/packages/desktop/resources/config/starter/komorebi.html similarity index 100% rename from packages/desktop/resources/starter/komorebi.html rename to packages/desktop/resources/config/starter/komorebi.html diff --git a/packages/desktop/resources/starter/komorebi.json b/packages/desktop/resources/config/starter/komorebi.json similarity index 52% rename from packages/desktop/resources/starter/komorebi.json rename to packages/desktop/resources/config/starter/komorebi.json index d4656dd3..0f9a4126 100644 --- a/packages/desktop/resources/starter/komorebi.json +++ b/packages/desktop/resources/config/starter/komorebi.json @@ -1,16 +1,17 @@ { - "html_path": "./glazewm.html", - "launch_options": { - "z_order": "normal", - "shown_in_taskbar": false, + "$schema": "TODO", + "htmlPath": "./komorebi.html", + "launchOptions": { + "zOrder": "normal", + "shownInTaskbar": false, "resizable": false, "transparent": true, "placement": [ { "monitor": 0, "anchor": "top_left", - "offset_x": "20px", - "offset_y": "20px", + "offsetX": "20px", + "offsetY": "20px", "width": "100%", "height": "30px" } diff --git a/packages/desktop/resources/starter/glazewm.yaml b/packages/desktop/resources/starter/glazewm.yaml deleted file mode 100644 index 31f23300..00000000 --- a/packages/desktop/resources/starter/glazewm.yaml +++ /dev/null @@ -1,41 +0,0 @@ -# Entry point HTML file. -html_path: './glazewm.html' - -# Default options for when the window is opened. -# These can be overridden when opening the window programmatically. -launch_options: - # Whether to show the window above/below all others. - # Allowed values: 'always_on_top', 'always_on_bottom', 'normal'. - z_order: 'normal' - - # Whether the window should be shown in the taskbar. - shown_in_taskbar: false - - # Whether the window should have resize handles. - resizable: false - - # Whether the window frame should be transparent. - transparent: true - - # Where to place the window. - placement: - # The monitor index to place the window on. - # Allowed values: 0, 1, 2, 3, etc. or 'all'. - - monitor: 0 - - # Anchor-point of the window. - # Allowed values: either 'center' or combinations of 'top', 'center', - # 'bottom' and 'left', 'center', 'right'. - anchor: 'top left' - - # Offset from the anchor-point. - offset_x: '20px' - - # Offset from the anchor-point. - offset_y: '20px' - - # Width of the window in % or physical pixels. - width: '100%' - - # Height of the window in % or physical pixels. - height: '30px' diff --git a/packages/desktop/resources/starter/komorebi.yaml b/packages/desktop/resources/starter/komorebi.yaml deleted file mode 100644 index a6074d1f..00000000 --- a/packages/desktop/resources/starter/komorebi.yaml +++ /dev/null @@ -1,41 +0,0 @@ -# Entry point HTML file. -html_path: './komorebi.html' - -# Default options for when the window is opened. -# These can be overridden when opening the window programmatically. -launch_options: - # Whether to show the window above/below all others. - # Allowed values: 'always_on_top', 'always_on_bottom', 'normal'. - z_order: 'normal' - - # Whether the window should be shown in the taskbar. - shown_in_taskbar: false - - # Whether the window should have resize handles. - resizable: false - - # Whether the window frame should be transparent. - transparent: true - - # Where to place the window. - placement: - # The monitor index to place the window on. - # Allowed values: 0, 1, 2, 3, etc. or 'all'. - - monitor: 0 - - # Anchor-point of the window. - # Allowed values: either 'center' or combinations of 'top', 'center', - # 'bottom' and 'left', 'center', 'right'. - anchor: 'top left' - - # Offset from the anchor-point. - offset_x: '20px' - - # Offset from the anchor-point. - offset_y: '20px' - - # Width of the window in % or physical pixels. - width: '100%' - - # Height of the window in % or physical pixels. - height: '30px' diff --git a/packages/desktop/src/user_config.rs b/packages/desktop/src/user_config.rs index 15a51556..5481e2be 100644 --- a/packages/desktop/src/user_config.rs +++ b/packages/desktop/src/user_config.rs @@ -10,6 +10,7 @@ use tauri::{path::BaseDirectory, AppHandle, Manager}; use crate::util::LengthValue; #[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] pub struct WindowConfig { /// Entry point HTML file. html_path: String, @@ -19,6 +20,7 @@ pub struct WindowConfig { } #[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] pub struct WindowLaunchOptions { /// Whether to show the window above/below all others. z_order: WindowZOrder, @@ -45,6 +47,7 @@ pub enum WindowZOrder { } #[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] pub struct WindowPlacement { /// The monitor index to place the window on. monitor: u32, @@ -82,9 +85,6 @@ pub enum WindowAnchor { #[derive(Debug)] pub struct Config { - /// Handle to the Tauri application. - // app_handle: AppHandle, - /// Directory where config files are stored. pub config_dir: PathBuf, @@ -114,10 +114,7 @@ impl Config { /// Reads the config files within the config directory. pub fn read(&mut self) -> anyhow::Result<()> { - // let mut configs = Vec::new(); - // Self::traverse_directory(self.config_dir, &mut configs)?; self.config_entries = Self::aggregate_configs(&self.config_dir)?; - Ok(()) } From de6f2fc83fed2bae25186ba8cf92cf484aa072ab Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Fri, 23 Aug 2024 17:10:05 +0800 Subject: [PATCH 018/138] chore: remove unused deps --- packages/client-api/package.json | 4 +--- pnpm-lock.yaml | 11 ----------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/packages/client-api/package.json b/packages/client-api/package.json index 8638e338..8b6adaff 100644 --- a/packages/client-api/package.json +++ b/packages/client-api/package.json @@ -24,9 +24,7 @@ "@tauri-apps/plugin-shell": "2.0.0-beta.8", "glazewm": "1.4.1", "luxon": "3.4.4", - "solid-js": "1.8.14", - "yaml": "2.3.4", - "zod": "3.22.4" + "solid-js": "1.8.14" }, "devDependencies": { "@types/luxon": "3.4.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f8280174..bd0fa3d9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,12 +66,6 @@ importers: solid-js: specifier: 1.8.14 version: 1.8.14 - yaml: - specifier: 2.3.4 - version: 2.3.4 - zod: - specifier: 3.22.4 - version: 3.22.4 devDependencies: '@types/luxon': specifier: 3.4.2 @@ -1759,9 +1753,6 @@ packages: resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==} engines: {node: '>= 14'} - zod@3.22.4: - resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} - snapshots: '@ampproject/remapping@2.2.1': @@ -3239,5 +3230,3 @@ snapshots: yallist@4.0.0: {} yaml@2.3.4: {} - - zod@3.22.4: {} From 17208c7e5c5384c43c5e82c8ff45d8c8da3eb261 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Fri, 23 Aug 2024 17:12:27 +0800 Subject: [PATCH 019/138] refactor: rename `user_config` mod -> `config` --- .../desktop/src/{user_config.rs => config.rs} | 48 ++++++------------- packages/desktop/src/main.rs | 8 ++-- 2 files changed, 19 insertions(+), 37 deletions(-) rename packages/desktop/src/{user_config.rs => config.rs} (85%) diff --git a/packages/desktop/src/user_config.rs b/packages/desktop/src/config.rs similarity index 85% rename from packages/desktop/src/user_config.rs rename to packages/desktop/src/config.rs index 5481e2be..8a181ee2 100644 --- a/packages/desktop/src/user_config.rs +++ b/packages/desktop/src/config.rs @@ -13,29 +13,29 @@ use crate::util::LengthValue; #[serde(rename_all = "camelCase")] pub struct WindowConfig { /// Entry point HTML file. - html_path: String, + pub html_path: String, /// Default options for when the window is opened. - launch_options: WindowLaunchOptions, + pub launch_options: WindowLaunchOptions, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct WindowLaunchOptions { /// Whether to show the window above/below all others. - z_order: WindowZOrder, + pub z_order: WindowZOrder, /// Whether the window should be shown in the taskbar. - shown_in_taskbar: bool, + pub shown_in_taskbar: bool, /// Whether the window should have resize handles. - resizable: bool, + pub resizable: bool, /// Whether the window frame should be transparent. - transparent: bool, + pub transparent: bool, /// Where to place the window. - placement: Vec, + pub placement: Vec, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -50,22 +50,22 @@ pub enum WindowZOrder { #[serde(rename_all = "camelCase")] pub struct WindowPlacement { /// The monitor index to place the window on. - monitor: u32, + pub monitor: u32, /// Anchor-point of the window. - anchor: WindowAnchor, + pub anchor: WindowAnchor, /// Offset from the anchor-point. - offset_x: LengthValue, + pub offset_x: LengthValue, /// Offset from the anchor-point. - offset_y: LengthValue, + pub offset_y: LengthValue, /// Width of the window in % or physical pixels. - width: LengthValue, + pub width: LengthValue, /// Height of the window in % or physical pixels. - height: LengthValue, + pub height: LengthValue, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -94,8 +94,8 @@ pub struct Config { #[derive(Debug)] pub struct ConfigEntry { - path: PathBuf, - config: WindowConfig, + pub path: PathBuf, + pub config: WindowConfig, } impl Config { @@ -154,24 +154,6 @@ impl Config { ) } - // fn traverse_directory( - // dir: &PathBuf, - // config_entries: &mut Vec, - // ) -> anyhow::Result<()> { - // for entry in fs::read_dir(dir)? { - // let entry = entry?; - // let path = entry.path(); - // if path.is_dir() { - // Self::traverse_directory(&path, config_entries)?; - // } else if Self::is_json_file(&entry)? { - // if let Ok(config_entry) = Self::process_file(entry) { - // config_entries.push(config_entry); - // } - // } - // } - // Ok(()) - // } - fn process_file(entry: &PathBuf) -> anyhow::Result { let content = fs::read_to_string(entry)?; let config = serde_json::from_str(&content)?; diff --git a/packages/desktop/src/main.rs b/packages/desktop/src/main.rs index ad85ad23..b6be1409 100644 --- a/packages/desktop/src/main.rs +++ b/packages/desktop/src/main.rs @@ -1,14 +1,14 @@ #![feature(async_closure)] -use std::{collections::HashMap, env, sync::Arc}; +use std::{collections::HashMap, env}; use clap::Parser; +use config::Config; use tauri::{Manager, State, Window}; use tracing::level_filters::LevelFilter; use tracing_subscriber::EnvFilter; -use user_config::Config; use crate::{ - cli::{Cli, CliCommand, OpenWindowArgs, OutputMonitorsArgs}, + cli::{Cli, CliCommand, OutputMonitorsArgs}, monitors::get_monitors_str, providers::{config::ProviderConfig, provider_manager::ProviderManager}, sys_tray::setup_sys_tray, @@ -17,10 +17,10 @@ use crate::{ }; mod cli; +mod config; mod monitors; mod providers; mod sys_tray; -mod user_config; mod util; mod window_factory; From 31a836ed2a57c607002e8c5fe457d68628b39df1 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Fri, 23 Aug 2024 17:43:54 +0800 Subject: [PATCH 020/138] refactor: clean up config processing --- packages/desktop/src/config.rs | 89 ++++++++++++++++++++-------------- 1 file changed, 53 insertions(+), 36 deletions(-) diff --git a/packages/desktop/src/config.rs b/packages/desktop/src/config.rs index 8a181ee2..ef14ae1b 100644 --- a/packages/desktop/src/config.rs +++ b/packages/desktop/src/config.rs @@ -1,5 +1,5 @@ use std::{ - fs::{self, DirEntry}, + fs::{self}, path::PathBuf, }; @@ -9,6 +9,13 @@ use tauri::{path::BaseDirectory, AppHandle, Manager}; use crate::util::LengthValue; +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SettingsConfig { + /// Relative paths to Zebar window configs to launch on start. + pub launch_on_start: Vec, +} + #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct WindowConfig { @@ -88,12 +95,15 @@ pub struct Config { /// Directory where config files are stored. pub config_dir: PathBuf, - /// Config entries. - pub config_entries: Vec, + /// Global settings. + // pub settings: SettingsConfig, + + /// List of window configs. + pub window_configs: Vec, } #[derive(Debug)] -pub struct ConfigEntry { +pub struct WindowConfigEntry { pub path: PathBuf, pub config: WindowConfig, } @@ -104,37 +114,44 @@ impl Config { let config_dir = app_handle .path() .resolve(".glzr/zebar", BaseDirectory::Home) - .context("Unable to get home directory.")?; + .context("Unable to get home directory.")? + .canonicalize()?; Ok(Self { config_dir, - config_entries: Vec::new(), + window_configs: Vec::new(), }) } /// Reads the config files within the config directory. pub fn read(&mut self) -> anyhow::Result<()> { - self.config_entries = Self::aggregate_configs(&self.config_dir)?; + self.window_configs = Self::aggregate_configs(&self.config_dir)?; Ok(()) } - fn aggregate_configs(dir: &PathBuf) -> anyhow::Result> { + /// Recursively aggregates all valid window configs in the given + /// directory and its subdirectories. + /// + /// Returns a list of `ConfigEntry` instances. + fn aggregate_configs( + dir: &PathBuf, + ) -> anyhow::Result> { let mut configs = Vec::new(); - for entry in fs::read_dir(dir).with_context(|| { + let entries = fs::read_dir(dir).with_context(|| { format!("Failed to read directory: {}", dir.display()) - })? { + })?; + + for entry in entries { let entry = entry?; let path = entry.path(); if path.is_dir() { - // Recursively call aggregate_configs on subdirectories + // Recursively aggregate configs on subdirectories. configs.extend(Self::aggregate_configs(&path)?); - } else if path.extension().and_then(|ext| ext.to_str()) - == Some("json") - { + } else if Self::is_json(&path) { if let Ok(config) = Self::process_file(&path) { - configs.push(ConfigEntry { + configs.push(WindowConfigEntry { config, path: path.clone(), }); @@ -145,24 +162,29 @@ impl Config { Ok(configs) } - fn is_json_file(entry: &DirEntry) -> anyhow::Result { - let file_type = entry.file_type()?; - Ok( - file_type.is_file() - && entry.path().extension().and_then(|ext| ext.to_str()) - == Some("json"), - ) + /// Returns whether the given path is a JSON file. + fn is_json(path: &PathBuf) -> bool { + path.extension().and_then(|ext| ext.to_str()) == Some("json") } - fn process_file(entry: &PathBuf) -> anyhow::Result { - let content = fs::read_to_string(entry)?; - let config = serde_json::from_str(&content)?; + /// Processes a single JSON file, reading its contents and parsing it. + /// + /// Returns the parsed `WindowConfig` if successful. + fn process_file(path: &PathBuf) -> anyhow::Result { + let content = fs::read_to_string(path).with_context(|| { + format!("Failed to read file: {}", path.display()) + })?; + + let config = serde_json::from_str(&content).with_context(|| { + format!("Failed to parse JSON from file: {}", path.display()) + })?; + Ok(config) } /// Initialize config at the given path from the sample config resource. fn create_from_sample( - config_path: &PathBuf, + &self, app_handle: &AppHandle, ) -> anyhow::Result<()> { let sample_path = app_handle @@ -198,24 +220,19 @@ impl Config { Ok(()) } - pub fn open_config_dir(app_handle: &AppHandle) -> anyhow::Result<()> { - let dir_path = app_handle - .path() - .resolve(".glzr/zebar", BaseDirectory::Home) - .context("Unable to get home directory.")? - .canonicalize()?; - + /// Opens the config directory in the OS-dependent file explorer. + pub fn open_config_dir(&self) -> anyhow::Result<()> { #[cfg(target_os = "windows")] { std::process::Command::new("explorer") - .arg(dir_path) + .arg(self.config_dir.clone()) .spawn()?; } #[cfg(target_os = "macos")] { std::process::Command::new("open") - .arg(dir_path) + .arg(self.config_dir.clone()) .arg("-R") .spawn()?; } @@ -223,7 +240,7 @@ impl Config { #[cfg(target_os = "linux")] { std::process::Command::new("xdg-open") - .arg(dir_path) + .arg(self.config_dir.clone()) .spawn()?; } From 62b2d2890315d0538a78161767a18a7a2e221444 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Fri, 23 Aug 2024 20:05:56 +0800 Subject: [PATCH 021/138] feat: evaluate settings file and init starter config if it doesn't exist --- packages/desktop/src/config.rs | 128 +++++++++++++++++++++++---------- packages/desktop/src/main.rs | 3 +- 2 files changed, 93 insertions(+), 38 deletions(-) diff --git a/packages/desktop/src/config.rs b/packages/desktop/src/config.rs index ef14ae1b..f05c924d 100644 --- a/packages/desktop/src/config.rs +++ b/packages/desktop/src/config.rs @@ -6,6 +6,7 @@ use std::{ use anyhow::Context; use serde::{Deserialize, Serialize}; use tauri::{path::BaseDirectory, AppHandle, Manager}; +use tracing::{info, warn}; use crate::util::LengthValue; @@ -96,7 +97,7 @@ pub struct Config { pub config_dir: PathBuf, /// Global settings. - // pub settings: SettingsConfig, + pub settings: SettingsConfig, /// List of window configs. pub window_configs: Vec, @@ -109,7 +110,8 @@ pub struct WindowConfigEntry { } impl Config { - /// Creates a new `Config` instance. + /// Reads the config files within the config directory and creates a new + /// `Config` instance. pub fn new(app_handle: &AppHandle) -> anyhow::Result { let config_dir = app_handle .path() @@ -117,23 +119,75 @@ impl Config { .context("Unable to get home directory.")? .canonicalize()?; + let settings = Self::read_settings_or_init(app_handle, &config_dir)?; + let window_configs = Self::read_window_configs(&config_dir)?; + Ok(Self { config_dir, - window_configs: Vec::new(), + settings, + window_configs, }) } - /// Reads the config files within the config directory. - pub fn read(&mut self) -> anyhow::Result<()> { - self.window_configs = Self::aggregate_configs(&self.config_dir)?; - Ok(()) + /// Re-evaluates config files within the config directory. + // pub fn reload(&mut self) -> anyhow::Result<()> { + // self.settings = Self::read_settings_or_init(&self.config_dir)?; + // self.window_configs = Self::read_window_configs(&self.config_dir)?; + // Ok(()) + // } + + /// Reads the global settings file or initializes it with the starter. + /// + /// Returns the parsed `SettingsConfig`. + fn read_settings_or_init( + app_handle: &AppHandle, + dir: &PathBuf, + ) -> anyhow::Result { + let settings = Self::read_settings(&dir)?; + + match settings { + Some(settings) => Ok(settings), + None => { + Self::create_from_starter(app_handle, dir)?; + Ok(Self::read_settings(&dir)?.unwrap()) + } + } + } + + /// Reads the global settings file. + /// + /// Returns the parsed `SettingsConfig` if found. + fn read_settings( + dir: &PathBuf, + ) -> anyhow::Result> { + let settings_path = dir.join("settings.json"); + + match settings_path.exists() { + true => { + let content = + fs::read_to_string(&settings_path).with_context(|| { + format!("Failed to read file: {}", settings_path.display()) + })?; + + let settings = + serde_json::from_str(&content).with_context(|| { + format!( + "Failed to parse JSON from file: {}", + settings_path.display() + ) + })?; + + Ok(Some(settings)) + } + false => Ok(None), + } } /// Recursively aggregates all valid window configs in the given - /// directory and its subdirectories. + /// directory. /// /// Returns a list of `ConfigEntry` instances. - fn aggregate_configs( + fn read_window_configs( dir: &PathBuf, ) -> anyhow::Result> { let mut configs = Vec::new(); @@ -147,14 +201,18 @@ impl Config { let path = entry.path(); if path.is_dir() { - // Recursively aggregate configs on subdirectories. - configs.extend(Self::aggregate_configs(&path)?); + // Recursively aggregate configs in subdirectories. + configs.extend(Self::read_window_configs(&path)?); } else if Self::is_json(&path) { if let Ok(config) = Self::process_file(&path) { + info!("Found valid window config at: {}", path.display()); configs.push(WindowConfigEntry { config, path: path.clone(), }); + } else { + // TODO: Show error dialog. + warn!("Failed to process file: {}", path.display()); } } } @@ -182,40 +240,38 @@ impl Config { Ok(config) } - /// Initialize config at the given path from the sample config resource. - fn create_from_sample( - &self, + /// Initialize config at the given path from the starter resource. + fn create_from_starter( app_handle: &AppHandle, + config_dir: &PathBuf, ) -> anyhow::Result<()> { - let sample_path = app_handle + let starter_path = app_handle .path() - .resolve("resources/sample-config.yaml", BaseDirectory::Resource) + .resolve("resources/config", BaseDirectory::Resource) .context("Unable to resolve sample config resource.")?; - let sample_script = app_handle - .path() - .resolve("resources/script.js", BaseDirectory::Resource) - .context("Unable to resolve sample script resource.")?; - - let dest_dir = - config_path.parent().context("Invalid config directory.")?; - // Create the destination directory. - std::fs::create_dir_all(&dest_dir).with_context(|| { - format!("Unable to create directory {}.", &config_path.display()) + fs::create_dir_all(&config_dir).with_context(|| { + format!("Unable to create directory {}.", &config_dir.display()) })?; - // Copy over sample config. - let config_path = dest_dir.join("config.yaml"); - fs::copy(&sample_path, &config_path).with_context(|| { - format!("Unable to write to {}.", config_path.display()) - })?; + Self::copy_dir_all(&starter_path, config_dir)?; - // Copy over sample script. - let script_path = dest_dir.join("script.js"); - fs::copy(&sample_script, &script_path).with_context(|| { - format!("Unable to write to {}.", script_path.display()) - })?; + Ok(()) + } + + fn copy_dir_all(src: &PathBuf, dest: &PathBuf) -> anyhow::Result<()> { + fs::create_dir_all(&dest)?; + + for entry in fs::read_dir(src)? { + let entry = entry?; + + if entry.file_type()?.is_dir() { + Self::copy_dir_all(&entry.path(), &dest.join(entry.file_name()))?; + } else { + fs::copy(entry.path(), dest.join(entry.file_name()))?; + } + } Ok(()) } diff --git a/packages/desktop/src/main.rs b/packages/desktop/src/main.rs index b6be1409..9cd27769 100644 --- a/packages/desktop/src/main.rs +++ b/packages/desktop/src/main.rs @@ -137,8 +137,7 @@ fn start_app(cli: Cli) { tauri::Builder::default() .setup(|app| { - let mut config = Config::new(app.handle())?; - config.read()?; + let config = Config::new(app.handle())?; println!("{:?}", config); app.manage(config); From eb128f93ae5ce74bb619aaeaf024f31b131e4be8 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Fri, 23 Aug 2024 20:17:02 +0800 Subject: [PATCH 022/138] feat: add `reload` fn; add generic `read_and_parse_json` fn --- packages/desktop/src/config.rs | 51 +++++++++++++++------------------- packages/desktop/src/main.rs | 1 - 2 files changed, 23 insertions(+), 29 deletions(-) diff --git a/packages/desktop/src/config.rs b/packages/desktop/src/config.rs index f05c924d..9ce08d01 100644 --- a/packages/desktop/src/config.rs +++ b/packages/desktop/src/config.rs @@ -4,7 +4,7 @@ use std::{ }; use anyhow::Context; -use serde::{Deserialize, Serialize}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; use tauri::{path::BaseDirectory, AppHandle, Manager}; use tracing::{info, warn}; @@ -93,6 +93,9 @@ pub enum WindowAnchor { #[derive(Debug)] pub struct Config { + /// Handle to the Tauri application. + app_handle: AppHandle, + /// Directory where config files are stored. pub config_dir: PathBuf, @@ -123,6 +126,7 @@ impl Config { let window_configs = Self::read_window_configs(&config_dir)?; Ok(Self { + app_handle: app_handle.clone(), config_dir, settings, window_configs, @@ -130,11 +134,14 @@ impl Config { } /// Re-evaluates config files within the config directory. - // pub fn reload(&mut self) -> anyhow::Result<()> { - // self.settings = Self::read_settings_or_init(&self.config_dir)?; - // self.window_configs = Self::read_window_configs(&self.config_dir)?; - // Ok(()) - // } + pub fn reload(&mut self) -> anyhow::Result<()> { + self.settings = + Self::read_settings_or_init(&self.app_handle, &self.config_dir)?; + + self.window_configs = Self::read_window_configs(&self.config_dir)?; + + Ok(()) + } /// Reads the global settings file or initializes it with the starter. /// @@ -163,23 +170,8 @@ impl Config { let settings_path = dir.join("settings.json"); match settings_path.exists() { - true => { - let content = - fs::read_to_string(&settings_path).with_context(|| { - format!("Failed to read file: {}", settings_path.display()) - })?; - - let settings = - serde_json::from_str(&content).with_context(|| { - format!( - "Failed to parse JSON from file: {}", - settings_path.display() - ) - })?; - - Ok(Some(settings)) - } false => Ok(None), + true => Self::read_and_parse_json(&settings_path), } } @@ -204,8 +196,9 @@ impl Config { // Recursively aggregate configs in subdirectories. configs.extend(Self::read_window_configs(&path)?); } else if Self::is_json(&path) { - if let Ok(config) = Self::process_file(&path) { + if let Ok(config) = Self::read_and_parse_json(&path) { info!("Found valid window config at: {}", path.display()); + configs.push(WindowConfigEntry { config, path: path.clone(), @@ -225,19 +218,21 @@ impl Config { path.extension().and_then(|ext| ext.to_str()) == Some("json") } - /// Processes a single JSON file, reading its contents and parsing it. + /// Reads a JSON file and parses it into the specified type. /// - /// Returns the parsed `WindowConfig` if successful. - fn process_file(path: &PathBuf) -> anyhow::Result { + /// Returns the parsed type `T` if successful. + fn read_and_parse_json( + path: &PathBuf, + ) -> anyhow::Result { let content = fs::read_to_string(path).with_context(|| { format!("Failed to read file: {}", path.display()) })?; - let config = serde_json::from_str(&content).with_context(|| { + let parsed = serde_json::from_str(&content).with_context(|| { format!("Failed to parse JSON from file: {}", path.display()) })?; - Ok(config) + Ok(parsed) } /// Initialize config at the given path from the starter resource. diff --git a/packages/desktop/src/main.rs b/packages/desktop/src/main.rs index 9cd27769..7635ac81 100644 --- a/packages/desktop/src/main.rs +++ b/packages/desktop/src/main.rs @@ -138,7 +138,6 @@ fn start_app(cli: Cli) { tauri::Builder::default() .setup(|app| { let config = Config::new(app.handle())?; - println!("{:?}", config); app.manage(config); // If this is not the first instance of the app, this will From f67baa5d76b5a57f0bdc4d9eb44d48b36ab786b5 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Fri, 23 Aug 2024 20:21:00 +0800 Subject: [PATCH 023/138] refactor: move out fs helper functions to new `fs_util` mod --- packages/desktop/src/config.rs | 43 ++++------------------------ packages/desktop/src/util/fs_util.rs | 38 ++++++++++++++++++++++++ packages/desktop/src/util/mod.rs | 2 ++ 3 files changed, 45 insertions(+), 38 deletions(-) create mode 100644 packages/desktop/src/util/fs_util.rs diff --git a/packages/desktop/src/config.rs b/packages/desktop/src/config.rs index 9ce08d01..774e77b1 100644 --- a/packages/desktop/src/config.rs +++ b/packages/desktop/src/config.rs @@ -4,11 +4,11 @@ use std::{ }; use anyhow::Context; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde::{Deserialize, Serialize}; use tauri::{path::BaseDirectory, AppHandle, Manager}; use tracing::{info, warn}; -use crate::util::LengthValue; +use crate::util::{copy_dir_all, read_and_parse_json, LengthValue}; #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -171,7 +171,7 @@ impl Config { match settings_path.exists() { false => Ok(None), - true => Self::read_and_parse_json(&settings_path), + true => read_and_parse_json(&settings_path), } } @@ -196,7 +196,7 @@ impl Config { // Recursively aggregate configs in subdirectories. configs.extend(Self::read_window_configs(&path)?); } else if Self::is_json(&path) { - if let Ok(config) = Self::read_and_parse_json(&path) { + if let Ok(config) = read_and_parse_json(&path) { info!("Found valid window config at: {}", path.display()); configs.push(WindowConfigEntry { @@ -218,23 +218,6 @@ impl Config { path.extension().and_then(|ext| ext.to_str()) == Some("json") } - /// Reads a JSON file and parses it into the specified type. - /// - /// Returns the parsed type `T` if successful. - fn read_and_parse_json( - path: &PathBuf, - ) -> anyhow::Result { - let content = fs::read_to_string(path).with_context(|| { - format!("Failed to read file: {}", path.display()) - })?; - - let parsed = serde_json::from_str(&content).with_context(|| { - format!("Failed to parse JSON from file: {}", path.display()) - })?; - - Ok(parsed) - } - /// Initialize config at the given path from the starter resource. fn create_from_starter( app_handle: &AppHandle, @@ -250,23 +233,7 @@ impl Config { format!("Unable to create directory {}.", &config_dir.display()) })?; - Self::copy_dir_all(&starter_path, config_dir)?; - - Ok(()) - } - - fn copy_dir_all(src: &PathBuf, dest: &PathBuf) -> anyhow::Result<()> { - fs::create_dir_all(&dest)?; - - for entry in fs::read_dir(src)? { - let entry = entry?; - - if entry.file_type()?.is_dir() { - Self::copy_dir_all(&entry.path(), &dest.join(entry.file_name()))?; - } else { - fs::copy(entry.path(), dest.join(entry.file_name()))?; - } - } + copy_dir_all(&starter_path, config_dir)?; Ok(()) } diff --git a/packages/desktop/src/util/fs_util.rs b/packages/desktop/src/util/fs_util.rs new file mode 100644 index 00000000..8e34363f --- /dev/null +++ b/packages/desktop/src/util/fs_util.rs @@ -0,0 +1,38 @@ +use std::{fs, path::PathBuf}; + +use anyhow::Context; +use serde::de::DeserializeOwned; + +/// Reads a JSON file and parses it into the specified type. +/// +/// Returns the parsed type `T` if successful. +pub fn read_and_parse_json( + path: &PathBuf, +) -> anyhow::Result { + let content = fs::read_to_string(path) + .with_context(|| format!("Failed to read file: {}", path.display()))?; + + let parsed = serde_json::from_str(&content).with_context(|| { + format!("Failed to parse JSON from file: {}", path.display()) + })?; + + Ok(parsed) +} + +/// Recursively copies a directory and all its contents to a new file +/// location. +pub fn copy_dir_all(src: &PathBuf, dest: &PathBuf) -> anyhow::Result<()> { + fs::create_dir_all(&dest)?; + + for entry in fs::read_dir(src)? { + let entry = entry?; + + if entry.file_type()?.is_dir() { + copy_dir_all(&entry.path(), &dest.join(entry.file_name()))?; + } else { + fs::copy(entry.path(), dest.join(entry.file_name()))?; + } + } + + Ok(()) +} diff --git a/packages/desktop/src/util/mod.rs b/packages/desktop/src/util/mod.rs index 000b4f60..6976f4ef 100644 --- a/packages/desktop/src/util/mod.rs +++ b/packages/desktop/src/util/mod.rs @@ -1,5 +1,7 @@ +mod fs_util; mod length_value; mod window_ext; +pub use fs_util::*; pub use length_value::*; pub use window_ext::*; From d45fdeb865d8401be1a1e2b46c023896c225e727 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Fri, 23 Aug 2024 20:27:32 +0800 Subject: [PATCH 024/138] refactor: rename `util` mod -> `common` --- packages/desktop/src/{util => common}/fs_util.rs | 0 packages/desktop/src/{util => common}/length_value.rs | 0 packages/desktop/src/{util => common}/mod.rs | 0 packages/desktop/src/{util => common}/window_ext.rs | 0 packages/desktop/src/config.rs | 2 +- packages/desktop/src/main.rs | 4 ++-- packages/desktop/src/window_factory.rs | 2 +- 7 files changed, 4 insertions(+), 4 deletions(-) rename packages/desktop/src/{util => common}/fs_util.rs (100%) rename packages/desktop/src/{util => common}/length_value.rs (100%) rename packages/desktop/src/{util => common}/mod.rs (100%) rename packages/desktop/src/{util => common}/window_ext.rs (100%) diff --git a/packages/desktop/src/util/fs_util.rs b/packages/desktop/src/common/fs_util.rs similarity index 100% rename from packages/desktop/src/util/fs_util.rs rename to packages/desktop/src/common/fs_util.rs diff --git a/packages/desktop/src/util/length_value.rs b/packages/desktop/src/common/length_value.rs similarity index 100% rename from packages/desktop/src/util/length_value.rs rename to packages/desktop/src/common/length_value.rs diff --git a/packages/desktop/src/util/mod.rs b/packages/desktop/src/common/mod.rs similarity index 100% rename from packages/desktop/src/util/mod.rs rename to packages/desktop/src/common/mod.rs diff --git a/packages/desktop/src/util/window_ext.rs b/packages/desktop/src/common/window_ext.rs similarity index 100% rename from packages/desktop/src/util/window_ext.rs rename to packages/desktop/src/common/window_ext.rs diff --git a/packages/desktop/src/config.rs b/packages/desktop/src/config.rs index 774e77b1..fd09b0c2 100644 --- a/packages/desktop/src/config.rs +++ b/packages/desktop/src/config.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use tauri::{path::BaseDirectory, AppHandle, Manager}; use tracing::{info, warn}; -use crate::util::{copy_dir_all, read_and_parse_json, LengthValue}; +use crate::common::{copy_dir_all, read_and_parse_json, LengthValue}; #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] diff --git a/packages/desktop/src/main.rs b/packages/desktop/src/main.rs index 7635ac81..ba39efa5 100644 --- a/packages/desktop/src/main.rs +++ b/packages/desktop/src/main.rs @@ -9,19 +9,19 @@ use tracing_subscriber::EnvFilter; use crate::{ cli::{Cli, CliCommand, OutputMonitorsArgs}, + common::WindowExt, monitors::get_monitors_str, providers::{config::ProviderConfig, provider_manager::ProviderManager}, sys_tray::setup_sys_tray, - util::WindowExt, window_factory::{WindowFactory, WindowState}, }; mod cli; +mod common; mod config; mod monitors; mod providers; mod sys_tray; -mod util; mod window_factory; #[tauri::command] diff --git a/packages/desktop/src/window_factory.rs b/packages/desktop/src/window_factory.rs index ae8f997a..1ea49409 100644 --- a/packages/desktop/src/window_factory.rs +++ b/packages/desktop/src/window_factory.rs @@ -11,7 +11,7 @@ use tauri::{AppHandle, WebviewUrl, WebviewWindowBuilder}; use tokio::{sync::Mutex, task}; use tracing::{error, info}; -use crate::util::WindowExt; +use crate::common::WindowExt; /// Manages the creation of Zebar windows. pub struct WindowFactory { From c31a76d9abd687317b5676ec123e1ed6da30e70b Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Fri, 23 Aug 2024 20:31:33 +0800 Subject: [PATCH 025/138] feat: avoid overwriting existing config files when initializing starter --- packages/desktop/src/common/fs_util.rs | 31 ++++++++++++++++++++------ packages/desktop/src/config.rs | 20 +++++------------ 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/packages/desktop/src/common/fs_util.rs b/packages/desktop/src/common/fs_util.rs index 8e34363f..8e83f6e5 100644 --- a/packages/desktop/src/common/fs_util.rs +++ b/packages/desktop/src/common/fs_util.rs @@ -19,18 +19,35 @@ pub fn read_and_parse_json( Ok(parsed) } +/// Returns whether the given path is a JSON file. +pub fn is_json(path: &PathBuf) -> bool { + path.extension().and_then(|ext| ext.to_str()) == Some("json") +} + /// Recursively copies a directory and all its contents to a new file /// location. -pub fn copy_dir_all(src: &PathBuf, dest: &PathBuf) -> anyhow::Result<()> { - fs::create_dir_all(&dest)?; - - for entry in fs::read_dir(src)? { +/// +/// Optionally replaces existing files in the destination directory if +/// `override_existing` is `true`. +pub fn copy_dir_all( + src_dir: &PathBuf, + dest_dir: &PathBuf, + override_existing: bool, +) -> anyhow::Result<()> { + fs::create_dir_all(&dest_dir)?; + + for entry in fs::read_dir(src_dir)? { let entry = entry?; + let path = entry.path(); if entry.file_type()?.is_dir() { - copy_dir_all(&entry.path(), &dest.join(entry.file_name()))?; - } else { - fs::copy(entry.path(), dest.join(entry.file_name()))?; + copy_dir_all( + &path, + &dest_dir.join(entry.file_name()), + override_existing, + )?; + } else if override_existing || !path.exists() { + fs::copy(path, dest_dir.join(entry.file_name()))?; } } diff --git a/packages/desktop/src/config.rs b/packages/desktop/src/config.rs index fd09b0c2..811ed843 100644 --- a/packages/desktop/src/config.rs +++ b/packages/desktop/src/config.rs @@ -8,7 +8,9 @@ use serde::{Deserialize, Serialize}; use tauri::{path::BaseDirectory, AppHandle, Manager}; use tracing::{info, warn}; -use crate::common::{copy_dir_all, read_and_parse_json, LengthValue}; +use crate::common::{ + copy_dir_all, is_json, read_and_parse_json, LengthValue, +}; #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -195,7 +197,7 @@ impl Config { if path.is_dir() { // Recursively aggregate configs in subdirectories. configs.extend(Self::read_window_configs(&path)?); - } else if Self::is_json(&path) { + } else if is_json(&path) { if let Ok(config) = read_and_parse_json(&path) { info!("Found valid window config at: {}", path.display()); @@ -213,11 +215,6 @@ impl Config { Ok(configs) } - /// Returns whether the given path is a JSON file. - fn is_json(path: &PathBuf) -> bool { - path.extension().and_then(|ext| ext.to_str()) == Some("json") - } - /// Initialize config at the given path from the starter resource. fn create_from_starter( app_handle: &AppHandle, @@ -226,14 +223,9 @@ impl Config { let starter_path = app_handle .path() .resolve("resources/config", BaseDirectory::Resource) - .context("Unable to resolve sample config resource.")?; - - // Create the destination directory. - fs::create_dir_all(&config_dir).with_context(|| { - format!("Unable to create directory {}.", &config_dir.display()) - })?; + .context("Unable to resolve starter config resource.")?; - copy_dir_all(&starter_path, config_dir)?; + copy_dir_all(&starter_path, config_dir, false)?; Ok(()) } From 71ed01d8b815c239e2489c7cecc2c028362a90a8 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Sat, 24 Aug 2024 04:15:03 +0800 Subject: [PATCH 026/138] feat: progress on initializing window from config --- .../client-api/src/desktop/current-window.ts | 5 ++- .../src/desktop/desktop-commands.ts | 30 +++++-------- .../src/desktop/get-open-window-args.ts | 4 +- .../client-api/src/desktop/shared/index.ts | 2 +- .../desktop/shared/open-window-args.model.ts | 5 --- .../src/desktop/shared/window-state.model.ts | 9 ++++ packages/client-api/src/init.ts | 32 +++++++++++--- packages/client-api/src/shims.d.ts | 2 +- .../client-api/src/zebar-context.model.ts | 5 ++- packages/desktop/src/cli.rs | 18 -------- packages/desktop/src/config.rs | 17 +++++++ packages/desktop/src/main.rs | 29 ++++++------ packages/desktop/src/window_factory.rs | 44 +++++++++++-------- 13 files changed, 114 insertions(+), 88 deletions(-) delete mode 100644 packages/client-api/src/desktop/shared/open-window-args.model.ts create mode 100644 packages/client-api/src/desktop/shared/window-state.model.ts diff --git a/packages/client-api/src/desktop/current-window.ts b/packages/client-api/src/desktop/current-window.ts index a1b93b07..ed35922f 100644 --- a/packages/client-api/src/desktop/current-window.ts +++ b/packages/client-api/src/desktop/current-window.ts @@ -66,7 +66,10 @@ export async function setWindowStyles(styles: Partial) { ]); } -async function setWindowZOrder(window: Window, zOrder?: WindowZOrder) { +export async function setWindowZOrder( + window: Window, + zOrder?: WindowZOrder, +) { if (zOrder === 'always_on_bottom') { await window.setAlwaysOnBottom(true); } else if (zOrder === 'always_on_top') { diff --git a/packages/client-api/src/desktop/desktop-commands.ts b/packages/client-api/src/desktop/desktop-commands.ts index 9fb80cc1..1a185440 100644 --- a/packages/client-api/src/desktop/desktop-commands.ts +++ b/packages/client-api/src/desktop/desktop-commands.ts @@ -4,34 +4,24 @@ import { } from '@tauri-apps/api/core'; import { createLogger } from '../utils'; -import type { ProviderConfig } from '~/user-config'; -import type { OpenWindowArgs } from './shared'; +import type { WindowState } from './shared'; +import type { ProviderConfig } from '~/providers'; const logger = createLogger('desktop-commands'); /** - * Reads config file from disk. Creates file if it doesn't exist. + * Get state associated with the given {@link windowId}. */ -export function readConfigFile(): Promise { - return invoke('read_config_file'); -} - -/** - * Get args used to open the window with the {@link windowLabel}. - */ -export function getInitialState( - windowLabel: string, -): Promise { - return invoke('get_open_window_args', { - windowLabel, +export function getWindowState( + windowId: string, +): Promise { + return invoke('get_window_state', { + windowId, }); } -export function openWindow( - windowId: string, - args: Record = {}, -): Promise { - return invoke('open_window', { windowId, args }); +export function openWindow(configPath: string): Promise { + return invoke('open_window', { configPath }); } // TODO: Add support for only fetching selected variables. diff --git a/packages/client-api/src/desktop/get-open-window-args.ts b/packages/client-api/src/desktop/get-open-window-args.ts index e00d79cd..d2c91f26 100644 --- a/packages/client-api/src/desktop/get-open-window-args.ts +++ b/packages/client-api/src/desktop/get-open-window-args.ts @@ -1,6 +1,6 @@ import { getCurrentWindow } from '@tauri-apps/api/window'; -import { getInitialState } from './desktop-commands'; +import { getWindowState } from './desktop-commands'; let promise: Promise | null = null; @@ -13,5 +13,5 @@ async function fetchOpenWindowArgs() { return window.__ZEBAR_INITIAL_STATE; } - return getInitialState(getCurrentWindow().label); + return getWindowState(getCurrentWindow().label); } diff --git a/packages/client-api/src/desktop/shared/index.ts b/packages/client-api/src/desktop/shared/index.ts index d9573444..07e227a5 100644 --- a/packages/client-api/src/desktop/shared/index.ts +++ b/packages/client-api/src/desktop/shared/index.ts @@ -1,2 +1,2 @@ export * from './monitor-info.model'; -export * from './open-window-args.model'; +export * from './window-state.model'; diff --git a/packages/client-api/src/desktop/shared/open-window-args.model.ts b/packages/client-api/src/desktop/shared/open-window-args.model.ts deleted file mode 100644 index 04c76e46..00000000 --- a/packages/client-api/src/desktop/shared/open-window-args.model.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface OpenWindowArgs { - args: Record; - env: Record; - windowId: string; -} diff --git a/packages/client-api/src/desktop/shared/window-state.model.ts b/packages/client-api/src/desktop/shared/window-state.model.ts new file mode 100644 index 00000000..f348c881 --- /dev/null +++ b/packages/client-api/src/desktop/shared/window-state.model.ts @@ -0,0 +1,9 @@ +import type { WindowConfig } from '~/user-config'; + +export interface WindowState { + windowId: string; + + config: WindowConfig; + + configPath: string; +} diff --git a/packages/client-api/src/init.ts b/packages/client-api/src/init.ts index 95524890..01b4a5f2 100644 --- a/packages/client-api/src/init.ts +++ b/packages/client-api/src/init.ts @@ -1,7 +1,13 @@ import { getCurrentWindow } from '@tauri-apps/api/window'; +import { join } from '@tauri-apps/api/path'; import { createRoot, getOwner, runWithOwner } from 'solid-js'; -import { getInitialState, openWindow, showErrorDialog } from './desktop'; +import { + getWindowState, + openWindow, + setWindowZOrder, + showErrorDialog, +} from './desktop'; import { createLogger } from '~/utils'; import type { ZebarContext } from './zebar-context.model'; import { createProvider } from './providers'; @@ -32,9 +38,9 @@ export async function init( try { const currentWindow = getCurrentWindow(); - const initialState = + const windowState = window.__ZEBAR_INITIAL_STATE ?? - (await getInitialState(currentWindow.label)); + (await getWindowState(currentWindow.label)); // Load default CSS unless explicitly disabled. if (options?.includeDefaultCss !== false) { @@ -45,13 +51,25 @@ export async function init( // @ts-ignore - TODO return { - // @ts-ignore - TODO - config: initialState.config, - openWindow, + openWindow: async (configPath: string) => { + const absolutePath = await join( + windowState.windowId, + '../', + configPath, + ); + + return openWindow(absolutePath); + }, createProvider: config => { return createProvider(config, getOwner()!); }, - currentWindow: {}, + currentWindow: { + ...windowState, + tauri: currentWindow, + setZOrder: zOrder => { + return setWindowZOrder(currentWindow, zOrder); + }, + }, allWindows: [], currentMonitor: {}, allMonitors: [], diff --git a/packages/client-api/src/shims.d.ts b/packages/client-api/src/shims.d.ts index ed83ef9b..b1dc5a88 100644 --- a/packages/client-api/src/shims.d.ts +++ b/packages/client-api/src/shims.d.ts @@ -1,6 +1,6 @@ interface Window { __TAURI__: any; - __ZEBAR_INITIAL_STATE: import('./desktop').OpenWindowArgs; + __ZEBAR_INITIAL_STATE: import('./desktop').WindowState; } declare module '*.css' { diff --git a/packages/client-api/src/zebar-context.model.ts b/packages/client-api/src/zebar-context.model.ts index 61992c6f..6951de12 100644 --- a/packages/client-api/src/zebar-context.model.ts +++ b/packages/client-api/src/zebar-context.model.ts @@ -37,8 +37,11 @@ export interface ZebarContext { } export interface ZebarWindow { + windowId: string; + config: WindowConfig; + configPath: string; tauri: TauriWindow; - setZOrder: (zOrder: WindowZOrder) => Promise; + setZOrder(zOrder: WindowZOrder): Promise; } export interface Monitor { diff --git a/packages/desktop/src/cli.rs b/packages/desktop/src/cli.rs index 02027e7d..bbd2bec2 100644 --- a/packages/desktop/src/cli.rs +++ b/packages/desktop/src/cli.rs @@ -32,12 +32,6 @@ pub enum CliCommand { pub struct OpenWindowArgs { /// Relative file path within the Zebar config directory. pub config_path: String, - - /// Arguments to pass to the window. - /// - /// These become available via the `self` provider. - #[clap(short, long, num_args = 1.., value_parser=parse_open_args)] - pub args: Option>, } #[derive(Args, Debug)] @@ -63,15 +57,3 @@ pub fn print_and_exit(output: anyhow::Result) { } } } - -/// Parses arguments passed to the `open` CLI command into a string tuple. -fn parse_open_args( - input: &str, -) -> anyhow::Result<(String, String), String> { - let mut parts = input.split('='); - - match (parts.next(), parts.next()) { - (Some(key), Some(value)) => Ok((key.into(), value.into())), - _ => Err("Arguments must be of format KEY1=VAL1".into()), - } -} diff --git a/packages/desktop/src/config.rs b/packages/desktop/src/config.rs index 811ed843..0b9541e6 100644 --- a/packages/desktop/src/config.rs +++ b/packages/desktop/src/config.rs @@ -230,6 +230,23 @@ impl Config { Ok(()) } + /// Returns the window config at the given absolute path. + pub fn window_config_by_path( + &self, + config_path: &str, + ) -> anyhow::Result> { + let config_pathbuf = PathBuf::from(config_path).canonicalize()?; + + let config_entry = self + .window_configs + .iter() + .find(|entry| entry.path == config_pathbuf); + + let config = config_entry.map(|entry| entry.config.clone()); + + Ok(config) + } + /// Opens the config directory in the OS-dependent file explorer. pub fn open_config_dir(&self) -> anyhow::Result<()> { #[cfg(target_os = "windows")] diff --git a/packages/desktop/src/main.rs b/packages/desktop/src/main.rs index ba39efa5..fc024c77 100644 --- a/packages/desktop/src/main.rs +++ b/packages/desktop/src/main.rs @@ -1,6 +1,7 @@ #![feature(async_closure)] -use std::{collections::HashMap, env}; +use std::env; +use anyhow::Context; use clap::Parser; use config::Config; use tauri::{Manager, State, Window}; @@ -25,23 +26,25 @@ mod sys_tray; mod window_factory; #[tauri::command] -async fn get_open_window_args( - window_label: String, +async fn get_window_state( + window_id: String, window_factory: State<'_, WindowFactory>, - config: State<'_, Config>, ) -> anyhow::Result, String> { - println!("{:?}", config.config_dir); - Ok(window_factory.state_by_window_label(window_label).await) + Ok(window_factory.state_by_id(&window_id).await) } #[tauri::command] -fn open_window( +async fn open_window( config_path: String, - args: HashMap, + config: State<'_, Config>, window_factory: State<'_, WindowFactory>, ) -> anyhow::Result<(), String> { - // TODO: Pass in `WindowConfig`. - window_factory.try_open(); + let window_config = config + .window_config_by_path(&config_path) + .map_err(|err| err.to_string())? + .context("Window config not found.")?; + + window_factory.open_one(window_config); Ok(()) } @@ -149,7 +152,7 @@ fn start_app(cli: Cli) { // CLI command is guaranteed to be an open command here. if let CliCommand::Open(args) = cli.command { - app.state::().try_open(); + app.state::().open_one(); } }, ))?; @@ -161,7 +164,7 @@ fn start_app(cli: Cli) { // Open window with the given args and initialize // `WindowFactory` in Tauri state. let window_factory = WindowFactory::new(app.handle()); - window_factory.try_open(); + window_factory.open_one(); app.manage(window_factory); app.handle().plugin(tauri_plugin_shell::init())?; @@ -179,7 +182,7 @@ fn start_app(cli: Cli) { Ok(()) }) .invoke_handler(tauri::generate_handler![ - get_open_window_args, + get_window_state, open_window, listen_provider, unlisten_provider, diff --git a/packages/desktop/src/window_factory.rs b/packages/desktop/src/window_factory.rs index 1ea49409..8e61a1c0 100644 --- a/packages/desktop/src/window_factory.rs +++ b/packages/desktop/src/window_factory.rs @@ -11,7 +11,7 @@ use tauri::{AppHandle, WebviewUrl, WebviewWindowBuilder}; use tokio::{sync::Mutex, task}; use tracing::{error, info}; -use crate::common::WindowExt; +use crate::{common::WindowExt, config::WindowConfig}; /// Manages the creation of Zebar windows. pub struct WindowFactory { @@ -30,10 +30,16 @@ pub struct WindowFactory { #[derive(Serialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct WindowState { + /// Unique identifier for the window. + /// + /// Used as the window label. pub window_id: String, - pub window_label: String, - pub args: HashMap, - pub env: HashMap, + + /// User-defined config for the window. + pub config: WindowConfig, + + /// Absolute path to the window's config file. + pub config_path: String, } impl WindowFactory { @@ -45,23 +51,27 @@ impl WindowFactory { } } - /// TODO: Pass in `WindowConfig`. - pub fn try_open(&self) { + pub fn open_all(&self, configs: Vec) { + for config in configs { + self.open_one(config); + } + } + + pub fn open_one(&self, config: WindowConfig) { let app_handle = self.app_handle.clone(); let window_states = self.window_states.clone(); - let app_handle = app_handle.clone(); let window_count = self.window_count.clone(); task::spawn(async move { // Increment number of windows. let new_count = window_count.fetch_add(1, Ordering::Relaxed) + 1; - let open_res = Self::open(&app_handle, new_count); + let open_res = Self::create_window(&app_handle, new_count, config); match open_res { Ok(state) => { let mut window_states = window_states.lock().await; - window_states.insert(state.window_label.clone(), state); + window_states.insert(state.window_id.clone(), state); } Err(err) => { error!("Failed to open window: {:?}", err); @@ -70,9 +80,10 @@ impl WindowFactory { }); } - fn open( + fn create_window( app_handle: &AppHandle, window_count: u32, + config: WindowConfig, ) -> anyhow::Result { info!("Creating window #{}", window_count); @@ -95,9 +106,7 @@ impl WindowFactory { let state = WindowState { window_id: window_count.to_string(), - window_label: window_count.to_string(), - args: HashMap::new(), - env: std::env::vars().collect(), + config, }; _ = window.eval(&format!( @@ -113,11 +122,8 @@ impl WindowFactory { Ok(state) } - /// Gets an open window's state by a given window label. - pub async fn state_by_window_label( - &self, - window_label: String, - ) -> Option { - self.window_states.lock().await.get(&window_label).cloned() + /// Gets an open window's state by a given window ID. + pub async fn state_by_id(&self, window_id: &str) -> Option { + self.window_states.lock().await.get(window_id).cloned() } } From 393b71cd275e44fbe159db3eb483752be741d8e9 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Sat, 24 Aug 2024 05:01:29 +0800 Subject: [PATCH 027/138] feat: open window from local asset url --- .../resources/config/starter/glazewm.html | 1 + packages/desktop/src/config.rs | 2 +- packages/desktop/src/main.rs | 18 +++++++------ packages/desktop/src/window_factory.rs | 25 ++++++++++++------- 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/packages/desktop/resources/config/starter/glazewm.html b/packages/desktop/resources/config/starter/glazewm.html index 495ff786..cb79ec27 100644 --- a/packages/desktop/resources/config/starter/glazewm.html +++ b/packages/desktop/resources/config/starter/glazewm.html @@ -1,5 +1,6 @@ +

fjdisa

+ + diff --git a/examples/solidjs-ts/.gitignore b/examples/solidjs-ts/.gitignore new file mode 100644 index 00000000..0ca39c00 --- /dev/null +++ b/examples/solidjs-ts/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +.DS_Store diff --git a/examples/solidjs-ts/index.html b/examples/solidjs-ts/index.html new file mode 100644 index 00000000..7b4d990d --- /dev/null +++ b/examples/solidjs-ts/index.html @@ -0,0 +1,14 @@ + + + + + + + Zebar + + + +
+ + + diff --git a/examples/solidjs-ts/package.json b/examples/solidjs-ts/package.json new file mode 100644 index 00000000..775c5afb --- /dev/null +++ b/examples/solidjs-ts/package.json @@ -0,0 +1,19 @@ +{ + "name": "solidjs-ts", + "version": "0.0.0", + "description": "", + "scripts": { + "build": "vite build", + "dev": "vite", + "serve": "vite preview", + "start": "vite" + }, + "dependencies": { + "solid-js": "1.8.11" + }, + "devDependencies": { + "typescript": "5.3.3", + "vite": "5.0.11", + "vite-plugin-solid": "2.8.2" + } +} diff --git a/examples/solidjs-ts/tsconfig.json b/examples/solidjs-ts/tsconfig.json new file mode 100644 index 00000000..5d2faf0a --- /dev/null +++ b/examples/solidjs-ts/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "types": ["vite/client"], + "noEmit": true, + "isolatedModules": true + } +} diff --git a/examples/solidjs-ts/vite.config.ts b/examples/solidjs-ts/vite.config.ts new file mode 100644 index 00000000..7987ecbf --- /dev/null +++ b/examples/solidjs-ts/vite.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vite'; +import solidPlugin from 'vite-plugin-solid'; + +export default defineConfig({ + plugins: [solidPlugin()], + server: { port: 3000 }, + build: { target: 'esnext' }, +}); From 53527da2531660d8ef1a7cc75b521e158050edbc Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Sun, 1 Sep 2024 14:48:30 +0800 Subject: [PATCH 056/138] feat: improve solid-ts example --- .gitignore | 1 + examples/solidjs-ts/README.md | 36 ++++++++++ examples/solidjs-ts/src/App.module.css | 3 + examples/solidjs-ts/src/App.tsx | 21 ++++++ examples/solidjs-ts/src/index.tsx | 14 ++++ pnpm-lock.yaml | 94 ++++++++++++++++++++++++++ pnpm-workspace.yaml | 1 + 7 files changed, 170 insertions(+) create mode 100644 examples/solidjs-ts/README.md create mode 100644 examples/solidjs-ts/src/App.module.css create mode 100644 examples/solidjs-ts/src/App.tsx create mode 100644 examples/solidjs-ts/src/index.tsx diff --git a/.gitignore b/.gitignore index d3dd278b..94d9deec 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ gen .DS_Store *.pem *.env +examples/settings.json # Debug npm-debug.log* diff --git a/examples/solidjs-ts/README.md b/examples/solidjs-ts/README.md new file mode 100644 index 00000000..3168038f --- /dev/null +++ b/examples/solidjs-ts/README.md @@ -0,0 +1,36 @@ +## Usage + +Those templates dependencies are maintained via [pnpm](https://pnpm.io) via `pnpm up -Lri`. + +This is the reason you see a `pnpm-lock.yaml`. That being said, any package manager will work. This file can be safely be removed once you clone a template. + +```bash +$ npm install # or pnpm install or yarn install +``` + +### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs) + +## Available Scripts + +In the project directory, you can run: + +### `npm run dev` or `npm start` + +Runs the app in the development mode.
+Open [http://localhost:3000](http://localhost:3000) to view it in the browser. + +The page will reload if you make edits.
+ +### `npm run build` + +Builds the app for production to the `dist` folder.
+It correctly bundles Solid in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.
+Your app is ready to be deployed! + +## Deployment + +You can deploy the `dist` folder to any static host provider (netlify, surge, now, etc.) + +## This project was created with the [Solid CLI](https://solid-cli.netlify.app) diff --git a/examples/solidjs-ts/src/App.module.css b/examples/solidjs-ts/src/App.module.css new file mode 100644 index 00000000..de672762 --- /dev/null +++ b/examples/solidjs-ts/src/App.module.css @@ -0,0 +1,3 @@ +.app { + text-align: center; +} diff --git a/examples/solidjs-ts/src/App.tsx b/examples/solidjs-ts/src/App.tsx new file mode 100644 index 00000000..8d956cad --- /dev/null +++ b/examples/solidjs-ts/src/App.tsx @@ -0,0 +1,21 @@ +import styles from './App.module.css'; + +export function App() { + return ( +
+
+

+ Edit src/App.tsx and save to reload. +

+ + Learn Solid + +
+
+ ); +} diff --git a/examples/solidjs-ts/src/index.tsx b/examples/solidjs-ts/src/index.tsx new file mode 100644 index 00000000..842d2639 --- /dev/null +++ b/examples/solidjs-ts/src/index.tsx @@ -0,0 +1,14 @@ +/* @refresh reload */ +import { render } from 'solid-js/web'; + +import { App } from './App'; + +const root = document.getElementById('root'); + +if (import.meta.env.DEV && !(root instanceof HTMLElement)) { + throw new Error( + 'Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?', + ); +} + +render(() => , root!); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd0fa3d9..020db6aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,6 +15,22 @@ importers: specifier: 3.2.5 version: 3.2.5 + examples/solidjs-ts: + dependencies: + solid-js: + specifier: 1.8.11 + version: 1.8.11 + devDependencies: + typescript: + specifier: 5.3.3 + version: 5.3.3 + vite: + specifier: 5.0.11 + version: 5.0.11(@types/node@20.11.17)(sass@1.70.0) + vite-plugin-solid: + specifier: 2.8.2 + version: 2.8.2(solid-js@1.8.11)(vite@5.0.11(@types/node@20.11.17)(sass@1.70.0)) + packages/client: dependencies: morphdom: @@ -1460,6 +1476,9 @@ packages: resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} engines: {node: '>=12'} + solid-js@1.8.11: + resolution: {integrity: sha512-WdwmER+TwBJiN4rVQTVBxocg+9pKlOs41KzPYntrC86xO5sek8TzBYozPEZPL1IRWDouf2lMrvSbIs3CanlPvQ==} + solid-js@1.8.14: resolution: {integrity: sha512-kDfgHBm+ROVLDVuqaXh/jYz0ZVJ29TYfVsKsgDPtNcjoyaPtOvDX2l0tVnthjLdEXr7vDTYeqEYFfMkZakDsOQ==} @@ -1649,6 +1668,12 @@ packages: vue-tsc: optional: true + vite-plugin-solid@2.8.2: + resolution: {integrity: sha512-HcvMs6DTxBaO4kE3psnirPQBCUUdYeQkCNKuB2TpEkJsxb6BGP6/7qkbbCSMxn25PyNdjvzVi1WXi0ou8KPgHw==} + peerDependencies: + solid-js: ^1.7.2 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 + vite-plugin-solid@2.9.1: resolution: {integrity: sha512-RC4hj+lbvljw57BbMGDApvEOPEh14lwrr/GeXRLNQLcR1qnOdzOwwTSFy13Gj/6FNIZpBEl0bWPU+VYFawrqUw==} peerDependencies: @@ -1659,6 +1684,34 @@ packages: '@testing-library/jest-dom': optional: true + vite@5.0.11: + resolution: {integrity: sha512-XBMnDjZcNAw/G1gEiskiM1v6yzM4GE5aMGvhWTlHAYYhxb7S3/V1s3m2LDHa8Vh6yIWYYB0iJwsEaS523c4oYA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + vite@5.1.1: resolution: {integrity: sha512-wclpAgY3F1tR7t9LL5CcHC41YPkQIpKUGeIuT8MdNwNZr6OqOTLs7JX5vIHAtzqLWXts0T+GDrh9pN2arneKqg==} engines: {node: ^18.0.0 || >=20.0.0} @@ -2958,12 +3011,25 @@ snapshots: slash@4.0.0: {} + solid-js@1.8.11: + dependencies: + csstype: 3.1.3 + seroval: 1.0.4 + seroval-plugins: 1.0.4(seroval@1.0.4) + solid-js@1.8.14: dependencies: csstype: 3.1.3 seroval: 1.0.4 seroval-plugins: 1.0.4(seroval@1.0.4) + solid-refresh@0.6.3(solid-js@1.8.11): + dependencies: + '@babel/generator': 7.23.6 + '@babel/helper-module-imports': 7.22.15 + '@babel/types': 7.23.6 + solid-js: 1.8.11 + solid-refresh@0.6.3(solid-js@1.8.14): dependencies: '@babel/generator': 7.23.6 @@ -3141,6 +3207,20 @@ snapshots: optionalDependencies: typescript: 5.3.3 + vite-plugin-solid@2.8.2(solid-js@1.8.11)(vite@5.0.11(@types/node@20.11.17)(sass@1.70.0)): + dependencies: + '@babel/core': 7.23.7 + '@babel/preset-typescript': 7.23.3(@babel/core@7.23.7) + '@types/babel__core': 7.20.5 + babel-preset-solid: 1.8.9(@babel/core@7.23.7) + merge-anything: 5.1.7 + solid-js: 1.8.11 + solid-refresh: 0.6.3(solid-js@1.8.11) + vite: 5.0.11(@types/node@20.11.17)(sass@1.70.0) + vitefu: 0.2.5(vite@5.0.11(@types/node@20.11.17)(sass@1.70.0)) + transitivePeerDependencies: + - supports-color + vite-plugin-solid@2.9.1(solid-js@1.8.14)(vite@5.1.1(@types/node@20.11.17)(sass@1.70.0)): dependencies: '@babel/core': 7.23.7 @@ -3154,6 +3234,16 @@ snapshots: transitivePeerDependencies: - supports-color + vite@5.0.11(@types/node@20.11.17)(sass@1.70.0): + dependencies: + esbuild: 0.19.11 + postcss: 8.4.35 + rollup: 4.10.0 + optionalDependencies: + '@types/node': 20.11.17 + fsevents: 2.3.3 + sass: 1.70.0 + vite@5.1.1(@types/node@20.11.17)(sass@1.70.0): dependencies: esbuild: 0.19.11 @@ -3164,6 +3254,10 @@ snapshots: fsevents: 2.3.3 sass: 1.70.0 + vitefu@0.2.5(vite@5.0.11(@types/node@20.11.17)(sass@1.70.0)): + optionalDependencies: + vite: 5.0.11(@types/node@20.11.17)(sass@1.70.0) + vitefu@0.2.5(vite@5.1.1(@types/node@20.11.17)(sass@1.70.0)): optionalDependencies: vite: 5.1.1(@types/node@20.11.17)(sass@1.70.0) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 18ec407e..15989339 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,3 @@ packages: - 'packages/*' + - 'examples/*' From 21274bf362bca67eb02a4b344616a2a160fb8e67 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Sun, 1 Sep 2024 15:49:02 +0800 Subject: [PATCH 057/138] chore: add `.npmrc` for forcing examples to use local client-api --- .npmrc | 2 ++ examples/solidjs-ts/package.json | 3 ++- packages/client-api/package.json | 2 +- pnpm-lock.yaml | 3 +++ 4 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 .npmrc diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..6c2b9be4 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +link-workspace-packages=true +prefer-workspace-packages=true diff --git a/examples/solidjs-ts/package.json b/examples/solidjs-ts/package.json index 775c5afb..db2466f3 100644 --- a/examples/solidjs-ts/package.json +++ b/examples/solidjs-ts/package.json @@ -9,7 +9,8 @@ "start": "vite" }, "dependencies": { - "solid-js": "1.8.11" + "solid-js": "1.8.11", + "zebar": "^2.0.0" }, "devDependencies": { "typescript": "5.3.3", diff --git a/packages/client-api/package.json b/packages/client-api/package.json index 8b6adaff..bdc21401 100644 --- a/packages/client-api/package.json +++ b/packages/client-api/package.json @@ -1,6 +1,6 @@ { "name": "zebar", - "version": "0.0.0", + "version": "2.0.0", "type": "module", "exports": { "import": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 020db6aa..b409678e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: solid-js: specifier: 1.8.11 version: 1.8.11 + zebar: + specifier: ^2.0.0 + version: link:../../packages/client-api devDependencies: typescript: specifier: 5.3.3 From 0c8edc92c06b53d151644852243d0f91080fb7e0 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Sun, 1 Sep 2024 16:40:53 +0800 Subject: [PATCH 058/138] feat: be able to override config dir; use `examples` as default config dir for dev script --- .gitignore | 1 + packages/desktop/package.json | 2 +- .../resources/config/starter/glazewm.json | 2 +- .../resources/config/starter/komorebi.json | 4 ++-- packages/desktop/src/cli.rs | 22 ++++++++++++++++--- packages/desktop/src/config.rs | 16 +++++++++----- packages/desktop/src/main.rs | 14 +++++++++++- packages/desktop/tauri.conf.json | 3 +-- 8 files changed, 49 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index 94d9deec..6941e923 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ gen *.pem *.env examples/settings.json +examples/starter # Debug npm-debug.log* diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 794d8e06..fe9df5dc 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "scripts": { "build": "npm run tauri build -- --verbose", - "dev": "npm run tauri dev -- -- -- open-all", + "dev": "npm run tauri dev -- -- -- open-all --config-dir=../../examples", "format": "cargo fmt", "lint": "cargo fmt --check", "tauri": "tauri" diff --git a/packages/desktop/resources/config/starter/glazewm.json b/packages/desktop/resources/config/starter/glazewm.json index b0e4fd87..f782022c 100644 --- a/packages/desktop/resources/config/starter/glazewm.json +++ b/packages/desktop/resources/config/starter/glazewm.json @@ -6,7 +6,7 @@ "shownInTaskbar": false, "focused": false, "resizable": false, - "transparent": true, + "transparent": false, "placements": [ { "anchor": "top_left", diff --git a/packages/desktop/resources/config/starter/komorebi.json b/packages/desktop/resources/config/starter/komorebi.json index b0e4fd87..15a517fb 100644 --- a/packages/desktop/resources/config/starter/komorebi.json +++ b/packages/desktop/resources/config/starter/komorebi.json @@ -1,12 +1,12 @@ { "$schema": "TODO", - "htmlPath": "./glazewm.html", + "htmlPath": "./komorebi.html", "launchOptions": { "zOrder": "normal", "shownInTaskbar": false, "focused": false, "resizable": false, - "transparent": true, + "transparent": false, "placements": [ { "anchor": "top_left", diff --git a/packages/desktop/src/cli.rs b/packages/desktop/src/cli.rs index 6e357f3f..22bc77ae 100644 --- a/packages/desktop/src/cli.rs +++ b/packages/desktop/src/cli.rs @@ -1,4 +1,4 @@ -use std::process; +use std::{path::PathBuf, process}; use clap::{Args, Parser, Subcommand}; @@ -30,7 +30,7 @@ pub enum CliCommand { /// Open all default windows. /// /// Starts Zebar if it is not already running. - OpenAll, + OpenAll(OpenAllWindowsArgs), /// Output available monitors. Monitors(OutputMonitorsArgs), @@ -45,8 +45,24 @@ pub enum CliCommand { #[derive(Args, Clone, Debug)] pub struct OpenWindowArgs { - /// Relative file path within the Zebar config directory. + /// Relative file path to window config within the Zebar config + /// directory. pub config_path: String, + + /// Absolute path to the Zebar config directory. + /// + /// The default path is `%userprofile%/.glzr/zebar/` + #[clap(long, value_hint = clap::ValueHint::FilePath)] + pub config_dir: Option, +} + +#[derive(Args, Clone, Debug)] +pub struct OpenAllWindowsArgs { + /// Absolute path to the Zebar config directory. + /// + /// The default path is `%userprofile%/.glzr/zebar/` + #[clap(long, value_hint = clap::ValueHint::FilePath)] + pub config_dir: Option, } #[derive(Args, Clone, Debug)] diff --git a/packages/desktop/src/config.rs b/packages/desktop/src/config.rs index da5d02c1..524b358b 100644 --- a/packages/desktop/src/config.rs +++ b/packages/desktop/src/config.rs @@ -144,11 +144,17 @@ impl Config { /// Reads the config files within the config directory. /// /// Returns a new `Config` instance. - pub fn new(app_handle: &AppHandle) -> anyhow::Result { - let config_dir = app_handle - .path() - .resolve(".glzr/zebar", BaseDirectory::Home) - .context("Unable to get home directory.")?; + pub fn new( + app_handle: &AppHandle, + config_dir_override: Option, + ) -> anyhow::Result { + let config_dir = match config_dir_override { + Some(dir) => dir, + None => app_handle + .path() + .resolve(".glzr/zebar", BaseDirectory::Home) + .context("Unable to get home directory.")?, + }; let settings = Self::read_settings_or_init(app_handle, &config_dir)?; let window_configs = Self::read_window_configs(&config_dir)?; diff --git a/packages/desktop/src/main.rs b/packages/desktop/src/main.rs index 8956c1dd..56e7db78 100644 --- a/packages/desktop/src/main.rs +++ b/packages/desktop/src/main.rs @@ -79,8 +79,15 @@ fn start_app(cli: Cli) -> anyhow::Result<()> { .setup(move |app| { task::block_in_place(|| { block_on(async move { + let config_dir_override = match cli.command() { + CliCommand::Open(args) => args.config_dir, + CliCommand::OpenAll(args) => args.config_dir, + _ => None, + }; + // Initialize `Config` in Tauri state. - let config = Arc::new(Config::new(app.handle())?); + let config = + Arc::new(Config::new(app.handle(), config_dir_override)?); app.manage(config.clone()); // Initialize `MonitorState` in Tauri state. @@ -141,6 +148,11 @@ fn start_app(cli: Cli) -> anyhow::Result<()> { #[cfg(target_os = "macos")] app.set_activation_policy(tauri::ActivationPolicy::Accessory); + // Allow assets to be resolved from the config directory. + app + .asset_protocol_scope() + .allow_directory(&config.config_dir, true)?; + // Get window configs to open on start. let window_configs = match cli.command() { CliCommand::Open(args) => { diff --git a/packages/desktop/tauri.conf.json b/packages/desktop/tauri.conf.json index 8f26164a..dd105b21 100644 --- a/packages/desktop/tauri.conf.json +++ b/packages/desktop/tauri.conf.json @@ -45,8 +45,7 @@ "img-src": "'self' asset: http://asset.localhost blob: data: *" }, "assetProtocol": { - "enable": true, - "scope": ["$HOME/.glzr/zebar/**"] + "enable": true } } } From e13bfab1335b71d8272bf4536776f804afac53fe Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Sun, 1 Sep 2024 18:24:03 +0800 Subject: [PATCH 059/138] feat: integrate zebar in `solidjs-ts` example --- .../solidjs-buildless/my-window.zebar.json | 23 +++++++++++++++++++ examples/solidjs-ts/my-window.zebar.json | 23 +++++++++++++++++++ examples/solidjs-ts/src/App.tsx | 9 +++++++- examples/solidjs-ts/src/index.tsx | 6 ++++- examples/solidjs-ts/vite.config.ts | 1 + packages/client-api/src/index.ts | 5 ++-- packages/desktop/capabilities/main.json | 8 +++++-- 7 files changed, 69 insertions(+), 6 deletions(-) create mode 100644 examples/solidjs-buildless/my-window.zebar.json create mode 100644 examples/solidjs-ts/my-window.zebar.json diff --git a/examples/solidjs-buildless/my-window.zebar.json b/examples/solidjs-buildless/my-window.zebar.json new file mode 100644 index 00000000..54a726b0 --- /dev/null +++ b/examples/solidjs-buildless/my-window.zebar.json @@ -0,0 +1,23 @@ +{ + "$schema": "TODO", + "htmlPath": "./index.html", + "launchOptions": { + "zOrder": "normal", + "shownInTaskbar": false, + "focused": false, + "resizable": false, + "transparent": false, + "placements": [ + { + "anchor": "top_left", + "offsetX": "20px", + "offsetY": "20px", + "width": "100%", + "height": "30px", + "monitorSelection": { + "type": "all" + } + } + ] + } +} diff --git a/examples/solidjs-ts/my-window.zebar.json b/examples/solidjs-ts/my-window.zebar.json new file mode 100644 index 00000000..88ce7806 --- /dev/null +++ b/examples/solidjs-ts/my-window.zebar.json @@ -0,0 +1,23 @@ +{ + "$schema": "TODO", + "htmlPath": "./dist/index.html", + "launchOptions": { + "zOrder": "normal", + "shownInTaskbar": false, + "focused": false, + "resizable": false, + "transparent": false, + "placements": [ + { + "anchor": "top_left", + "offsetX": "20px", + "offsetY": "20px", + "width": "100%", + "height": "30px", + "monitorSelection": { + "type": "all" + } + } + ] + } +} diff --git a/examples/solidjs-ts/src/App.tsx b/examples/solidjs-ts/src/App.tsx index 8d956cad..9c0c01de 100644 --- a/examples/solidjs-ts/src/App.tsx +++ b/examples/solidjs-ts/src/App.tsx @@ -1,9 +1,16 @@ +import { ZebarContext } from 'zebar'; + import styles from './App.module.css'; -export function App() { +export async function App(ctx: ZebarContext) { + const cpu = await ctx.createProvider({ type: 'cpu' }); + const memory = await ctx.createProvider({ type: 'memory' }); + return (
+

{cpu.usage}

+

{memory.usage}

Edit src/App.tsx and save to reload.

diff --git a/examples/solidjs-ts/src/index.tsx b/examples/solidjs-ts/src/index.tsx index 842d2639..b9fb9034 100644 --- a/examples/solidjs-ts/src/index.tsx +++ b/examples/solidjs-ts/src/index.tsx @@ -1,5 +1,6 @@ /* @refresh reload */ import { render } from 'solid-js/web'; +import { init } from 'zebar'; import { App } from './App'; @@ -11,4 +12,7 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) { ); } -render(() => , root!); +init().then(async ctx => { + const app = await App(ctx); + render(() => app, root!); +}); diff --git a/examples/solidjs-ts/vite.config.ts b/examples/solidjs-ts/vite.config.ts index 7987ecbf..06d2b2e5 100644 --- a/examples/solidjs-ts/vite.config.ts +++ b/examples/solidjs-ts/vite.config.ts @@ -5,4 +5,5 @@ export default defineConfig({ plugins: [solidPlugin()], server: { port: 3000 }, build: { target: 'esnext' }, + base: './', }); diff --git a/packages/client-api/src/index.ts b/packages/client-api/src/index.ts index 2228ee93..8eca8acb 100644 --- a/packages/client-api/src/index.ts +++ b/packages/client-api/src/index.ts @@ -1,2 +1,3 @@ -export { type WindowConfig } from './user-config'; -export { init } from './init'; +export * from './user-config'; +export * from './zebar-context.model'; +export * from './init'; diff --git a/packages/desktop/capabilities/main.json b/packages/desktop/capabilities/main.json index 447cb5d6..316e9632 100644 --- a/packages/desktop/capabilities/main.json +++ b/packages/desktop/capabilities/main.json @@ -1,8 +1,11 @@ { "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", - "description": "Capability for the main window", - "windows": ["*-*"], + "description": "Default capabilities", + "windows": ["*"], + "remote": { + "urls": ["http://asset.localhost"] + }, "permissions": [ "app:default", "dialog:allow-message", @@ -13,6 +16,7 @@ "window:allow-center", "window:allow-close", "window:allow-hide", + "window:allow-show", "window:allow-maximize", "window:allow-minimize", "window:allow-set-skip-taskbar", From 00fd9c72d596e6b83567be8f093d8907d38bc3bc Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Mon, 2 Sep 2024 03:29:35 +0800 Subject: [PATCH 060/138] fix: change config structs to use camelCase --- packages/desktop/src/providers/battery/config.rs | 2 +- packages/desktop/src/providers/cpu/config.rs | 2 +- packages/desktop/src/providers/host/config.rs | 2 +- packages/desktop/src/providers/ip/config.rs | 2 +- packages/desktop/src/providers/komorebi/config.rs | 2 +- packages/desktop/src/providers/memory/config.rs | 2 +- packages/desktop/src/providers/network/config.rs | 2 +- packages/desktop/src/providers/weather/config.rs | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/desktop/src/providers/battery/config.rs b/packages/desktop/src/providers/battery/config.rs index 5836bc25..cb3c8084 100644 --- a/packages/desktop/src/providers/battery/config.rs +++ b/packages/desktop/src/providers/battery/config.rs @@ -3,7 +3,7 @@ use serde::Deserialize; use crate::impl_interval_config; #[derive(Deserialize, Debug)] -#[serde(tag = "type", rename = "battery")] +#[serde(rename_all = "camelCase")] pub struct BatteryProviderConfig { pub refresh_interval: u64, } diff --git a/packages/desktop/src/providers/cpu/config.rs b/packages/desktop/src/providers/cpu/config.rs index aae3966d..21afadb6 100644 --- a/packages/desktop/src/providers/cpu/config.rs +++ b/packages/desktop/src/providers/cpu/config.rs @@ -3,7 +3,7 @@ use serde::Deserialize; use crate::impl_interval_config; #[derive(Deserialize, Debug)] -#[serde(tag = "type", rename = "cpu")] +#[serde(rename_all = "camelCase")] pub struct CpuProviderConfig { pub refresh_interval: u64, } diff --git a/packages/desktop/src/providers/host/config.rs b/packages/desktop/src/providers/host/config.rs index 56e688fa..96e79ee9 100644 --- a/packages/desktop/src/providers/host/config.rs +++ b/packages/desktop/src/providers/host/config.rs @@ -3,7 +3,7 @@ use serde::Deserialize; use crate::impl_interval_config; #[derive(Deserialize, Debug)] -#[serde(tag = "type", rename = "host")] +#[serde(rename_all = "camelCase")] pub struct HostProviderConfig { pub refresh_interval: u64, } diff --git a/packages/desktop/src/providers/ip/config.rs b/packages/desktop/src/providers/ip/config.rs index e88e4709..ffe6ea14 100644 --- a/packages/desktop/src/providers/ip/config.rs +++ b/packages/desktop/src/providers/ip/config.rs @@ -3,7 +3,7 @@ use serde::Deserialize; use crate::impl_interval_config; #[derive(Deserialize, Debug)] -#[serde(tag = "type", rename = "ip")] +#[serde(rename_all = "camelCase")] pub struct IpProviderConfig { pub refresh_interval: u64, } diff --git a/packages/desktop/src/providers/komorebi/config.rs b/packages/desktop/src/providers/komorebi/config.rs index acdf8e83..1ee2e048 100644 --- a/packages/desktop/src/providers/komorebi/config.rs +++ b/packages/desktop/src/providers/komorebi/config.rs @@ -1,5 +1,5 @@ use serde::Deserialize; #[derive(Deserialize, Debug)] -#[serde(tag = "type", rename = "komorebi")] +#[serde(rename_all = "camelCase")] pub struct KomorebiProviderConfig {} diff --git a/packages/desktop/src/providers/memory/config.rs b/packages/desktop/src/providers/memory/config.rs index de322925..b71b8649 100644 --- a/packages/desktop/src/providers/memory/config.rs +++ b/packages/desktop/src/providers/memory/config.rs @@ -3,7 +3,7 @@ use serde::Deserialize; use crate::impl_interval_config; #[derive(Deserialize, Debug)] -#[serde(tag = "type", rename = "memory")] +#[serde(rename_all = "camelCase")] pub struct MemoryProviderConfig { pub refresh_interval: u64, } diff --git a/packages/desktop/src/providers/network/config.rs b/packages/desktop/src/providers/network/config.rs index 1bd7b0fd..873f08e8 100644 --- a/packages/desktop/src/providers/network/config.rs +++ b/packages/desktop/src/providers/network/config.rs @@ -3,7 +3,7 @@ use serde::Deserialize; use crate::impl_interval_config; #[derive(Deserialize, Debug)] -#[serde(tag = "type", rename = "network")] +#[serde(rename_all = "camelCase")] pub struct NetworkProviderConfig { pub refresh_interval: u64, } diff --git a/packages/desktop/src/providers/weather/config.rs b/packages/desktop/src/providers/weather/config.rs index 5c31026d..3a1e78cc 100644 --- a/packages/desktop/src/providers/weather/config.rs +++ b/packages/desktop/src/providers/weather/config.rs @@ -3,7 +3,7 @@ use serde::Deserialize; use crate::impl_interval_config; #[derive(Deserialize, Debug)] -#[serde(tag = "type", rename = "weather")] +#[serde(rename_all = "camelCase")] pub struct WeatherProviderConfig { pub refresh_interval: u64, pub latitude: f32, From 3605c3b6ae65110971a2a22891b7a190c0c2ff3e Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Mon, 2 Sep 2024 03:39:36 +0800 Subject: [PATCH 061/138] feat: progress on figuring out a nice client-side api for creating providers --- examples/solidjs-ts/package.json | 4 +- examples/solidjs-ts/src/App.tsx | 20 ++++- examples/solidjs-ts/src/App2.tsx | 106 ++++++++++++++++++++++++ examples/solidjs-ts/src/index.tsx | 132 ++++++++++++++++++++++++++++-- 4 files changed, 251 insertions(+), 11 deletions(-) create mode 100644 examples/solidjs-ts/src/App2.tsx diff --git a/examples/solidjs-ts/package.json b/examples/solidjs-ts/package.json index db2466f3..c489af28 100644 --- a/examples/solidjs-ts/package.json +++ b/examples/solidjs-ts/package.json @@ -4,9 +4,7 @@ "description": "", "scripts": { "build": "vite build", - "dev": "vite", - "serve": "vite preview", - "start": "vite" + "dev": "vite build --watch" }, "dependencies": { "solid-js": "1.8.11", diff --git a/examples/solidjs-ts/src/App.tsx b/examples/solidjs-ts/src/App.tsx index 9c0c01de..d7ac8a17 100644 --- a/examples/solidjs-ts/src/App.tsx +++ b/examples/solidjs-ts/src/App.tsx @@ -1,10 +1,24 @@ import { ZebarContext } from 'zebar'; import styles from './App.module.css'; +import { createEffect } from 'solid-js'; +import { createMutable } from 'solid-js/store'; -export async function App(ctx: ZebarContext) { - const cpu = await ctx.createProvider({ type: 'cpu' }); - const memory = await ctx.createProvider({ type: 'memory' }); +export default async function App(props: { ctx: ZebarContext }) { + const cpu = await props.ctx.createProvider({ type: 'cpu' }); + const memory = await props.ctx.createProvider({ type: 'memory' }); + + // const cpu = createMutable( + // await props.ctx.createProvider({ type: 'cpu' }), + // ); + // const memory = createMutable( + // await props.ctx.createProvider({ type: 'memory' }), + // ); + + createEffect(() => { + console.log('client', cpu); + console.log('client', cpu.usage); + }); return (
diff --git a/examples/solidjs-ts/src/App2.tsx b/examples/solidjs-ts/src/App2.tsx new file mode 100644 index 00000000..60137481 --- /dev/null +++ b/examples/solidjs-ts/src/App2.tsx @@ -0,0 +1,106 @@ +import { ZebarContext } from 'zebar'; + +import styles from './App.module.css'; +import { + Component, + createEffect, + createResource, + onCleanup, + onMount, + Show, + Suspense, +} from 'solid-js'; +import { createMutable, createStore } from 'solid-js/store'; + +export function App2(props: { ctx: ZebarContext }) { + const [cpu] = createResource(() => + props.ctx.createProvider({ type: 'cpu' }), + ); + + const [memory] = createResource(() => + props.ctx.createProvider({ type: 'memory' }), + ); + + // const cpu = createMutable( + // await props.ctx.createProvider({ type: 'cpu' }), + // ); + // const memory = createMutable( + // await props.ctx.createProvider({ type: 'memory' }), + // ); + + // const [providers] = createResource(async () => { + // return { + // cpu: await props.ctx.createProvider({ type: 'cpu' }), + // memory: await props.ctx.createProvider({ type: 'memory' }), + // }; + // }); + + // createEffect(() => { + // console.log('client', providers()); + // console.log('client', providers()?.cpu); + // }); + + // const [providers, setProviders] = createStore([]); + + // onMount(async () => { + // setProviders( + // await Promise.all([ + // props.ctx.createProvider({ type: 'cpu' }), + // props.ctx.createProvider({ type: 'memory' }), + // ]), + // ); + // }); + + createEffect(() => { + console.log('client', cpu()); + console.log('client', cpu()?.usage); + }); + + onCleanup(() => { + console.log('cleanup'); + }); + + return ( +
+ +

{cpu().usage}

+
+
+ ); + // return ( + //
+ //

{providers[0]?.usage}

+ //

{providers[0]?.usage}

+ //
+ // // + // // {(providers) => { + // //
+ // //

{cpu.usage}

+ // //

{memory.usage}

+ // //
; + // // }} + // //
+ // ); + // const [providers, setProviders] = createStore({ + // cpu: null, + // memory: null, + // loaded: false, + // }); + + // onMount(async () => { + // setProviders({ + // loaded: true, + // cpu: await props.ctx.createProvider({ type: 'cpu' }), + // memory: await props.ctx.createProvider({ type: 'memory' }), + // }); + // }); + + // return ( + // + //
+ //

{providers.cpu?.usage}

+ //

{providers.memory?.usage}

+ //
+ //
+ // ); +} diff --git a/examples/solidjs-ts/src/index.tsx b/examples/solidjs-ts/src/index.tsx index b9fb9034..8c7eacd9 100644 --- a/examples/solidjs-ts/src/index.tsx +++ b/examples/solidjs-ts/src/index.tsx @@ -1,8 +1,18 @@ /* @refresh reload */ import { render } from 'solid-js/web'; -import { init } from 'zebar'; +import { init, ZebarContext } from 'zebar'; -import { App } from './App'; +// import { App } from './App'; +import { + createEffect, + createResource, + createSignal, + lazy, + onMount, + Suspense, +} from 'solid-js'; +import { App2 } from './App2'; +import { createStore } from 'solid-js/store'; const root = document.getElementById('root'); @@ -12,7 +22,119 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) { ); } -init().then(async ctx => { - const app = await App(ctx); - render(() => app, root!); +// const xx = lazy(() => import('./App')); + +// init({}, ctx => { +// // const app = await App({ ctx }); +// // render(() => app, root!); +// // render(() => , root!); +// render(() => , root!); +// }); +// render(() => , root!); +init({}, async ctx => { + render(() => , root!); }); + +// function App() { +// const [context, setContext] = createSignal(); + +// const [aa] = createResource( +// () => context(), +// async context => { +// return { +// cpu: await context.createProvider({ type: 'cpu' }), +// memory: await context.createProvider({ type: 'memory' }), +// }; +// }, +// ); + +// init({}, ctx => setContext(ctx)); + +// const [providers, setProviders] = createStore(); + +// createEffect(async () => { +// if (context()) { +// setProviders( +// // await Promise.all([ +// // props.ctx.createProvider({ type: 'cpu' }), +// // props.ctx.createProvider({ type: 'memory' }), +// // ]), +// { +// cpu: await context().createProvider({ type: 'cpu' }), +// memory: await context().createProvider({ type: 'memory' }), +// }, +// ); +// console.log('aaa', providers); +// } +// }); + +// // return
Hello World {aa()?.cpu?.usage}
; +// return
Hello World {providers?.cpu?.usage}
; +// } + +// const ctx = await init(); + +// const providers = await ctx.createProviders({ +// memory: { type: 'memory' }, +// cpu: { type: 'cpu' }, +// glazewm: { type: 'glazewm' }, +// weather: { type: 'weather' }, +// }); + +// const cpu = await ctx.createProvider({ type: 'cpu' }); + +// async function bootstrap() { +// const ctx = await init(); +// const cpu = await ctx.createProvider({ type: 'cpu' }); +// const memory = await ctx.createProvider({ type: 'memory' }); +// // const providers = { cpu, memory }; + +// render(() => , root!); +// } + +// function App(props: { +// ctx: ZebarContext; +// providers: { cpu: any; memory: any }; +// }) { +// const [providers, setProviders] = createStore(providers); +// providers.onChange(setProviders); + +// return
Hello World {providers.cpu.usage}
; +// } + +const ctx = await init(); + +const [memory, cpu, glazewm, weather] = await ctx.createProviders([ + { type: 'memory' }, + { type: 'cpu' }, + { type: 'glazewm' }, + { type: 'weather' }, +]); + +render(() => , root!); + +function App() { + // const [memory, setMemory] = createStore(memory); + // const [cpu, setCpu] = createStore(cpu); + // const [glazewm, setGlazewm] = createStore(glazewm); + // const [weather, setWeather] = createStore(weather); + + // memory.onChange(setMemory); + // cpu.onChange(setCpu); + // glazewm.onChange(setGlazewm); + // weather.onChange(setWeather); + + const [providers, setProviders] = createStore({ + memory, + cpu, + glazewm, + weather, + }); + + memory.onChange(memory => setProviders({ memory })); + cpu.onChange(cpu => setProviders({ cpu })); + glazewm.onChange(glazewm => setProviders({ glazewm })); + weather.onChange(weather => setProviders({ weather })); + + return
Hello World {providers.cpu.usage}
; +} From 822bc6a8ae0bc69023ac1d845161df4e0f076d1a Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Mon, 2 Sep 2024 14:33:34 +0800 Subject: [PATCH 062/138] feat: add zod validation for provider configs --- packages/client-api/package.json | 3 ++- .../battery/create-battery-provider.ts | 10 +++++++++- .../src/providers/cpu/create-cpu-provider.ts | 12 ++++++++++-- .../src/providers/date/create-date-provider.ts | 18 ++++++++++++++---- .../glazewm/create-glazewm-provider.ts | 5 +++++ .../src/providers/host/create-host-provider.ts | 10 +++++++++- .../src/providers/ip/create-ip-provider.ts | 10 +++++++++- .../komorebi/create-komorebi-provider.ts | 8 +++++++- .../providers/memory/create-memory-provider.ts | 10 +++++++++- .../network/create-network-provider.ts | 10 +++++++++- .../src/providers/util/create-util-provider.ts | 5 +++++ .../weather/create-weather-provider.ts | 11 +++++++++-- pnpm-lock.yaml | 8 ++++++++ 13 files changed, 105 insertions(+), 15 deletions(-) diff --git a/packages/client-api/package.json b/packages/client-api/package.json index bdc21401..ddf17c8e 100644 --- a/packages/client-api/package.json +++ b/packages/client-api/package.json @@ -24,7 +24,8 @@ "@tauri-apps/plugin-shell": "2.0.0-beta.8", "glazewm": "1.4.1", "luxon": "3.4.4", - "solid-js": "1.8.14" + "solid-js": "1.8.14", + "zod": "3.22.4" }, "devDependencies": { "@types/luxon": "3.4.2", diff --git a/packages/client-api/src/providers/battery/create-battery-provider.ts b/packages/client-api/src/providers/battery/create-battery-provider.ts index 263c6eaf..1d23e471 100644 --- a/packages/client-api/src/providers/battery/create-battery-provider.ts +++ b/packages/client-api/src/providers/battery/create-battery-provider.ts @@ -1,4 +1,5 @@ import type { Owner } from 'solid-js'; +import { z } from 'zod'; import { createProviderListener } from '../create-provider-listener'; @@ -11,6 +12,11 @@ export interface BatteryProviderConfig { refreshInterval?: number; } +const BatteryProviderConfigSchema = z.object({ + type: z.literal('battery'), + refreshInterval: z.coerce.number().default(60 * 60 * 1000), +}); + export interface BatteryProvider { chargePercent: number; cycleCount: number; @@ -27,10 +33,12 @@ export async function createBatteryProvider( config: BatteryProviderConfig, owner: Owner, ) { + const mergedConfig = BatteryProviderConfigSchema.parse(config); + const batteryVariables = await createProviderListener< BatteryProviderConfig, BatteryProvider - >(config, owner); + >(mergedmergedConfig, owner); return { get chargePercent() { diff --git a/packages/client-api/src/providers/cpu/create-cpu-provider.ts b/packages/client-api/src/providers/cpu/create-cpu-provider.ts index a36a34c9..fd79462c 100644 --- a/packages/client-api/src/providers/cpu/create-cpu-provider.ts +++ b/packages/client-api/src/providers/cpu/create-cpu-provider.ts @@ -1,4 +1,5 @@ -import type { Owner } from 'solid-js'; +import { type Owner } from 'solid-js'; +import { z } from 'zod'; import { createProviderListener } from '../create-provider-listener'; @@ -11,6 +12,11 @@ export interface CpuProviderConfig { refreshInterval?: number; } +const CpuProviderConfigSchema = z.object({ + type: z.literal('cpu'), + refreshInterval: z.coerce.number().default(5 * 1000), +}); + export interface CpuProvider { frequency: number; usage: number; @@ -23,10 +29,12 @@ export async function createCpuProvider( config: CpuProviderConfig, owner: Owner, ) { + const mergedConfig = CpuProviderConfigSchema.parse(config); + const cpuVariables = await createProviderListener< CpuProviderConfig, CpuProvider - >(config, owner); + >(mergedConfig, owner); return { get frequency() { diff --git a/packages/client-api/src/providers/date/create-date-provider.ts b/packages/client-api/src/providers/date/create-date-provider.ts index f7f7452e..899fd2a1 100644 --- a/packages/client-api/src/providers/date/create-date-provider.ts +++ b/packages/client-api/src/providers/date/create-date-provider.ts @@ -1,6 +1,7 @@ import { DateTime } from 'luxon'; import { type Owner, onCleanup, runWithOwner } from 'solid-js'; import { createStore } from 'solid-js/store'; +import { z } from 'zod'; export interface DateProviderConfig { type: 'date'; @@ -28,6 +29,13 @@ export interface DateProviderConfig { locale?: string; } +const DateProviderConfigSchema = z.object({ + type: z.literal('date'), + refreshInterval: z.coerce.number().default(1000), + timezone: z.string().optional(), + locale: z.string().optional(), +}); + export interface DateProvider { /** * Current date/time as a JavaScript `Date` object. Uses `new Date()` under @@ -68,12 +76,14 @@ export async function createDateProvider( config: DateProviderConfig, owner: Owner, ) { + const mergedConfig = DateProviderConfigSchema.parse(config); + const [dateVariables, setDateVariables] = createStore(getDateVariables()); const interval = setInterval( () => setDateVariables(getDateVariables()), - config.refreshInterval, + mergedConfig.refreshInterval, ); runWithOwner(owner, () => { @@ -93,11 +103,11 @@ export async function createDateProvider( function toFormat(now: number, format: string) { let dateTime = DateTime.fromMillis(now); - if (config.timezone) { - dateTime = dateTime.setZone(config.timezone); + if (mergedConfig.timezone) { + dateTime = dateTime.setZone(mergedConfig.timezone); } - return dateTime.toFormat(format, { locale: config.locale }); + return dateTime.toFormat(format, { locale: mergedConfig.locale }); } return { diff --git a/packages/client-api/src/providers/glazewm/create-glazewm-provider.ts b/packages/client-api/src/providers/glazewm/create-glazewm-provider.ts index 8da41ceb..62ed0cab 100644 --- a/packages/client-api/src/providers/glazewm/create-glazewm-provider.ts +++ b/packages/client-api/src/providers/glazewm/create-glazewm-provider.ts @@ -16,6 +16,7 @@ import { } from 'glazewm'; import { createEffect, on, runWithOwner, type Owner } from 'solid-js'; import { createStore } from 'solid-js/store'; +import { z } from 'zod'; import { getMonitors } from '~/desktop'; import { getCoordinateDistance } from '~/utils'; @@ -24,6 +25,10 @@ export interface GlazeWmProviderConfig { type: 'glazewm'; } +const GlazeWmProviderConfigSchema = z.object({ + type: z.literal('glazewm'), +}); + export interface GlazeWmProvider { /** * Workspace displayed on the current monitor. diff --git a/packages/client-api/src/providers/host/create-host-provider.ts b/packages/client-api/src/providers/host/create-host-provider.ts index 5fd01203..5ee00b0e 100644 --- a/packages/client-api/src/providers/host/create-host-provider.ts +++ b/packages/client-api/src/providers/host/create-host-provider.ts @@ -1,4 +1,5 @@ import type { Owner } from 'solid-js'; +import { z } from 'zod'; import { createProviderListener } from '../create-provider-listener'; @@ -11,6 +12,11 @@ export interface HostProviderConfig { refreshInterval?: number; } +const HostProviderConfigSchema = z.object({ + type: z.literal('host'), + refreshInterval: z.coerce.number().default(60 * 1000), +}); + export interface HostProvider { hostname: string | null; osName: string | null; @@ -24,10 +30,12 @@ export async function createHostProvider( config: HostProviderConfig, owner: Owner, ) { + const mergedConfig = HostProviderConfigSchema.parse(config); + const hostVariables = await createProviderListener< HostProviderConfig, HostProvider - >(config, owner); + >(mergedConfig, owner); return { get hostname() { diff --git a/packages/client-api/src/providers/ip/create-ip-provider.ts b/packages/client-api/src/providers/ip/create-ip-provider.ts index 52b51d75..d79d1faa 100644 --- a/packages/client-api/src/providers/ip/create-ip-provider.ts +++ b/packages/client-api/src/providers/ip/create-ip-provider.ts @@ -1,4 +1,5 @@ import type { Owner } from 'solid-js'; +import { z } from 'zod'; import { createProviderListener } from '../create-provider-listener'; @@ -11,6 +12,11 @@ export interface IpProviderConfig { refreshInterval?: number; } +const IpProviderConfigSchema = z.object({ + type: z.literal('ip'), + refreshInterval: z.coerce.number().default(60 * 60 * 1000), +}); + export interface IpProvider { address: string; approxCity: string; @@ -23,10 +29,12 @@ export async function createIpProvider( config: IpProviderConfig, owner: Owner, ) { + const mergedConfig = IpProviderConfigSchema.parse(config); + const ipVariables = await createProviderListener< IpProviderConfig, IpProvider - >(config, owner); + >(mergedConfig, owner); return { get address() { diff --git a/packages/client-api/src/providers/komorebi/create-komorebi-provider.ts b/packages/client-api/src/providers/komorebi/create-komorebi-provider.ts index 44ac1c22..fb81a883 100644 --- a/packages/client-api/src/providers/komorebi/create-komorebi-provider.ts +++ b/packages/client-api/src/providers/komorebi/create-komorebi-provider.ts @@ -1,5 +1,6 @@ import { createEffect, runWithOwner, type Owner } from 'solid-js'; import { createStore } from 'solid-js/store'; +import { z } from 'zod'; import { createProviderListener } from '../create-provider-listener'; import { getMonitors } from '~/desktop'; @@ -9,6 +10,10 @@ export interface KomorebiProviderConfig { type: 'komorebi'; } +const KomorebiProviderConfigSchema = z.object({ + type: z.literal('komorebi'), +}); + export interface KomorebiProvider { /** * Workspace displayed on the current monitor. @@ -114,12 +119,13 @@ export async function createKomorebiProvider( config: KomorebiProviderConfig, owner: Owner, ): Promise { + const mergedConfig = KomorebiProviderConfigSchema.parse(config); const monitors = await getMonitors(); const providerListener = await createProviderListener< KomorebiProviderConfig, KomorebiResponse - >(config, owner); + >(mergedConfig, owner); const [komorebiVariables, setKomorebiVariables] = createStore( await getVariables(), diff --git a/packages/client-api/src/providers/memory/create-memory-provider.ts b/packages/client-api/src/providers/memory/create-memory-provider.ts index 97e6b1ba..ea0f667d 100644 --- a/packages/client-api/src/providers/memory/create-memory-provider.ts +++ b/packages/client-api/src/providers/memory/create-memory-provider.ts @@ -1,4 +1,5 @@ import type { Owner } from 'solid-js'; +import { z } from 'zod'; import { createProviderListener } from '../create-provider-listener'; @@ -11,6 +12,11 @@ export interface MemoryProviderConfig { refreshInterval?: number; } +const MemoryProviderConfigSchema = z.object({ + type: z.literal('memory'), + refreshInterval: z.coerce.number().default(5 * 1000), +}); + export interface MemoryProvider { usage: number; freeMemory: number; @@ -25,10 +31,12 @@ export async function createMemoryProvider( config: MemoryProviderConfig, owner: Owner, ) { + const mergedConfig = MemoryProviderConfigSchema.parse(config); + const memoryVariables = await createProviderListener< MemoryProviderConfig, MemoryProvider - >(config, owner); + >(mergedConfig, owner); return { get usage() { diff --git a/packages/client-api/src/providers/network/create-network-provider.ts b/packages/client-api/src/providers/network/create-network-provider.ts index 12c17e77..f551998e 100644 --- a/packages/client-api/src/providers/network/create-network-provider.ts +++ b/packages/client-api/src/providers/network/create-network-provider.ts @@ -1,4 +1,5 @@ import type { Owner } from 'solid-js'; +import { z } from 'zod'; import { createProviderListener } from '../create-provider-listener'; @@ -11,6 +12,11 @@ export interface NetworkProviderConfig { refreshInterval?: number; } +const NetworkProviderConfigSchema = z.object({ + type: z.literal('network'), + refreshInterval: z.coerce.number().default(5 * 1000), +}); + export interface NetworkProvider { defaultInterface: NetworkInterface | null; defaultGateway: NetworkGateway | null; @@ -68,10 +74,12 @@ export async function createNetworkProvider( config: NetworkProviderConfig, owner: Owner, ) { + const mergedConfig = NetworkProviderConfigSchema.parse(config); + const networkVariables = await createProviderListener< NetworkProviderConfig, NetworkProvider - >(config, owner); + >(mergedConfig, owner); return { get defaultInterface() { diff --git a/packages/client-api/src/providers/util/create-util-provider.ts b/packages/client-api/src/providers/util/create-util-provider.ts index 56b01cf4..18e46296 100644 --- a/packages/client-api/src/providers/util/create-util-provider.ts +++ b/packages/client-api/src/providers/util/create-util-provider.ts @@ -1,9 +1,14 @@ import type { Owner } from 'solid-js'; +import { z } from 'zod'; export interface UtilProviderConfig { type: 'util'; } +const UtilProviderConfigSchema = z.object({ + type: z.literal('util'), +}); + export interface UtilProvider { convertBytes( bytes: number, diff --git a/packages/client-api/src/providers/weather/create-weather-provider.ts b/packages/client-api/src/providers/weather/create-weather-provider.ts index f52b57a2..af768568 100644 --- a/packages/client-api/src/providers/weather/create-weather-provider.ts +++ b/packages/client-api/src/providers/weather/create-weather-provider.ts @@ -1,4 +1,5 @@ import type { Owner } from 'solid-js'; +import { z } from 'zod'; import { type IpProvider, @@ -28,6 +29,13 @@ export interface WeatherProviderConfig { refreshInterval?: number; } +const WeatherProviderConfigSchema = z.object({ + type: z.literal('weather'), + latitude: z.coerce.number().optional(), + longitude: z.coerce.number().optional(), + refreshInterval: z.coerce.number().default(60 * 60 * 1000), +}); + export interface WeatherProvider { isDaytime: boolean; status: WeatherStatus; @@ -43,8 +51,7 @@ export async function createWeatherProvider( let ipProvider: IpProvider | null = null; const mergedConfig: WeatherProviderConfig = { - ...config, - refreshInterval: config.refreshInterval ?? 60 * 60 * 1000, + ...WeatherProviderConfigSchema.parse(config), longitude: config.longitude ?? (await getIpProvider()).approxLongitude, latitude: config.latitude ?? (await getIpProvider()).approxLatitude, }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b409678e..ff6acbd3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -85,6 +85,9 @@ importers: solid-js: specifier: 1.8.14 version: 1.8.14 + zod: + specifier: 3.22.4 + version: 3.22.4 devDependencies: '@types/luxon': specifier: 3.4.2 @@ -1809,6 +1812,9 @@ packages: resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==} engines: {node: '>= 14'} + zod@3.22.4: + resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + snapshots: '@ampproject/remapping@2.2.1': @@ -3327,3 +3333,5 @@ snapshots: yallist@4.0.0: {} yaml@2.3.4: {} + + zod@3.22.4: {} From 4cfbc046131cb4bba03ad976adcbad042a70a7cb Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Mon, 2 Sep 2024 14:35:19 +0800 Subject: [PATCH 063/138] feat: remove solidjs reactivity from `createProviderListener` --- .../src/providers/create-provider-listener.ts | 64 ++++++++++--------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/packages/client-api/src/providers/create-provider-listener.ts b/packages/client-api/src/providers/create-provider-listener.ts index 914cdfd6..2ced3935 100644 --- a/packages/client-api/src/providers/create-provider-listener.ts +++ b/packages/client-api/src/providers/create-provider-listener.ts @@ -1,12 +1,3 @@ -import { - type Accessor, - createEffect, - createSignal, - onCleanup, - type Owner, - runWithOwner, -} from 'solid-js'; - import { onProviderEmit, listenProvider, @@ -15,36 +6,47 @@ import { import { simpleHash } from '~/utils'; import type { ProviderConfig } from './create-provider'; +export interface ProviderListener { + firstValue: TVars; + onChange: (callback: (val: TVars) => void) => void; + unlisten: () => void; +} + /** * Utility for listening to a provider of a given config type. */ -export function createProviderListener< +export async function createProviderListener< TConfig extends ProviderConfig, TVars, ->(config: TConfig, owner: Owner): Promise> { - return new Promise(async resolve => { - const [payload, setPayload] = createSignal(); +>(config: TConfig): Promise> { + const configHash = simpleHash(config); + const listeners: ((val: TVars) => void)[] = []; - const configHash = simpleHash(config); - const unlisten = await onProviderEmit(configHash, setPayload); + const unlistenEmit = await onProviderEmit(configHash, val => { + listeners.forEach(listener => listener(val)); + }); - await listenProvider({ - configHash, - config, - trackedAccess: [], + const firstValue = new Promise(async resolve => { + const unsubscribe = await onProviderEmit(configHash, value => { + unsubscribe(); + resolve(value); }); + }); - runWithOwner(owner, () => { - onCleanup(() => { - unlisten(); - unlistenProvider(configHash); - }); - - createEffect(() => { - if (payload()) { - resolve(payload as Accessor); - } - }); - }); + await listenProvider({ + configHash, + config, + trackedAccess: [], }); + + return { + firstValue: await firstValue, + onChange: (callback: (val: TVars) => void) => { + listeners.push(callback); + }, + unlisten: async () => { + unlistenEmit(); + await unlistenProvider(configHash); + }, + }; } From 9c6f52f359a6236202be6eaf61c04677f32d05de Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Mon, 2 Sep 2024 15:19:10 +0800 Subject: [PATCH 064/138] feat: create `Deferred` utility class --- .../src/providers/create-provider-listener.ts | 25 ++++++++----------- .../src/utils/create-shared-signal.ts | 11 -------- packages/client-api/src/utils/deferred.ts | 22 ++++++++++++++++ packages/client-api/src/utils/index.ts | 1 + 4 files changed, 33 insertions(+), 26 deletions(-) delete mode 100644 packages/client-api/src/utils/create-shared-signal.ts create mode 100644 packages/client-api/src/utils/deferred.ts diff --git a/packages/client-api/src/providers/create-provider-listener.ts b/packages/client-api/src/providers/create-provider-listener.ts index 2ced3935..ad08f2b4 100644 --- a/packages/client-api/src/providers/create-provider-listener.ts +++ b/packages/client-api/src/providers/create-provider-listener.ts @@ -3,36 +3,31 @@ import { listenProvider, unlistenProvider, } from '~/desktop'; -import { simpleHash } from '~/utils'; +import { Deferred, simpleHash } from '~/utils'; import type { ProviderConfig } from './create-provider'; export interface ProviderListener { firstValue: TVars; onChange: (callback: (val: TVars) => void) => void; - unlisten: () => void; + unlisten: () => Promise; } /** * Utility for listening to a provider of a given config type. */ -export async function createProviderListener< - TConfig extends ProviderConfig, - TVars, ->(config: TConfig): Promise> { +export async function createProviderListener( + config: ProviderConfig, +): Promise> { const configHash = simpleHash(config); + + const firstValue = new Deferred(); const listeners: ((val: TVars) => void)[] = []; const unlistenEmit = await onProviderEmit(configHash, val => { + firstValue.resolve(val); listeners.forEach(listener => listener(val)); }); - const firstValue = new Promise(async resolve => { - const unsubscribe = await onProviderEmit(configHash, value => { - unsubscribe(); - resolve(value); - }); - }); - await listenProvider({ configHash, config, @@ -40,8 +35,8 @@ export async function createProviderListener< }); return { - firstValue: await firstValue, - onChange: (callback: (val: TVars) => void) => { + firstValue: await firstValue.promise, + onChange: callback => { listeners.push(callback); }, unlisten: async () => { diff --git a/packages/client-api/src/utils/create-shared-signal.ts b/packages/client-api/src/utils/create-shared-signal.ts deleted file mode 100644 index 501c2e25..00000000 --- a/packages/client-api/src/utils/create-shared-signal.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { type Signal, createSignal } from 'solid-js'; - -const cache: Record> = {}; - -export function createSharedSignal(key: string, value: T): Signal { - if (cache[key]) { - return cache[key] as Signal; - } - - return (cache[key] = createSignal(value)); -} diff --git a/packages/client-api/src/utils/deferred.ts b/packages/client-api/src/utils/deferred.ts new file mode 100644 index 00000000..b75fd3fd --- /dev/null +++ b/packages/client-api/src/utils/deferred.ts @@ -0,0 +1,22 @@ +/** + * Utility for creating a promise that can be resolved and rejected outside + * the promise callback. + * + * @example + * ```ts + * const deferred = new Deferred(); + * deferred.resolve(42); + * ``` + */ +export class Deferred { + promise: Promise; + resolve!: (val: T | PromiseLike) => void; + reject!: (err: any) => void; + + constructor() { + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + } +} diff --git a/packages/client-api/src/utils/index.ts b/packages/client-api/src/utils/index.ts index b1cc1cf2..9171e576 100644 --- a/packages/client-api/src/utils/index.ts +++ b/packages/client-api/src/utils/index.ts @@ -1,4 +1,5 @@ export * from './types/prettify'; export * from './create-logger'; +export * from './deferred'; export * from './get-coordinate-distance'; export * from './simple-hash'; From c61a5884cd3bae00e192cde930a2eeebb41d12ce Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Mon, 2 Sep 2024 15:21:11 +0800 Subject: [PATCH 065/138] feat: remove solidjs reactive context; change battery + cpu providers to use updated `createProviderListener` --- packages/client-api/src/init.ts | 100 ++++++--------- .../battery/create-battery-provider.ts | 41 +----- .../src/providers/cpu/create-cpu-provider.ts | 35 ++---- .../src/providers/create-provider.ts | 117 +++++++++--------- packages/client-api/src/providers/index.ts | 18 +-- 5 files changed, 120 insertions(+), 191 deletions(-) diff --git a/packages/client-api/src/init.ts b/packages/client-api/src/init.ts index 01b4a5f2..2d1b1612 100644 --- a/packages/client-api/src/init.ts +++ b/packages/client-api/src/init.ts @@ -1,6 +1,5 @@ import { getCurrentWindow } from '@tauri-apps/api/window'; import { join } from '@tauri-apps/api/path'; -import { createRoot, getOwner, runWithOwner } from 'solid-js'; import { getWindowState, @@ -34,69 +33,52 @@ export interface ZebarInitOptions { export async function init( options?: ZebarInitOptions, ): Promise { - return withReactiveContext(async () => { - try { - const currentWindow = getCurrentWindow(); + try { + const currentWindow = getCurrentWindow(); - const windowState = - window.__ZEBAR_INITIAL_STATE ?? - (await getWindowState(currentWindow.label)); + const windowState = + window.__ZEBAR_INITIAL_STATE ?? + (await getWindowState(currentWindow.label)); - // Load default CSS unless explicitly disabled. - if (options?.includeDefaultCss !== false) { - import('./zebar.css'); - } - - await currentWindow.show(); + // Load default CSS unless explicitly disabled. + if (options?.includeDefaultCss !== false) { + import('./zebar.css'); + } - // @ts-ignore - TODO - return { - openWindow: async (configPath: string) => { - const absolutePath = await join( - windowState.windowId, - '../', - configPath, - ); + // @ts-ignore - TODO + return { + openWindow: async (configPath: string) => { + const absolutePath = await join( + windowState.configPath, + '../', + configPath, + ); - return openWindow(absolutePath); - }, - createProvider: config => { - return createProvider(config, getOwner()!); + return openWindow(absolutePath); + }, + createProvider, + currentWindow: { + ...windowState, + tauri: currentWindow, + setZOrder: zOrder => { + return setWindowZOrder(currentWindow, zOrder); }, - currentWindow: { - ...windowState, - tauri: currentWindow, - setZOrder: zOrder => { - return setWindowZOrder(currentWindow, zOrder); - }, - }, - allWindows: [], - currentMonitor: {}, - allMonitors: [], - } as ZebarContext; - } catch (err) { - logger.error('Failed to initialize window:', err); - - await showErrorDialog({ - title: 'Failed to initialize window', - error: err, - }); + }, + allWindows: [], + currentMonitor: {}, + allMonitors: [], + } as ZebarContext; + } catch (err) { + logger.error('Failed to initialize window:', err); - // Error during window initialization is unrecoverable, so we close - // the window. - getCurrentWindow().close(); - throw err; - } - }); -} - -/** - * Runs callback in a reactive context (allows for SolidJS reactivity). - */ -function withReactiveContext(callback: () => T) { - const owner = getOwner(); + await showErrorDialog({ + title: 'Failed to initialize window', + error: err, + }); - return owner - ? (runWithOwner(owner, callback) as T) - : createRoot(callback); + // Error during window initialization is unrecoverable, so we close + // the window. + getCurrentWindow().close(); + throw err; + } } diff --git a/packages/client-api/src/providers/battery/create-battery-provider.ts b/packages/client-api/src/providers/battery/create-battery-provider.ts index 1d23e471..9b6c2ff2 100644 --- a/packages/client-api/src/providers/battery/create-battery-provider.ts +++ b/packages/client-api/src/providers/battery/create-battery-provider.ts @@ -1,4 +1,3 @@ -import type { Owner } from 'solid-js'; import { z } from 'zod'; import { createProviderListener } from '../create-provider-listener'; @@ -31,42 +30,14 @@ export interface BatteryProvider { export async function createBatteryProvider( config: BatteryProviderConfig, - owner: Owner, ) { const mergedConfig = BatteryProviderConfigSchema.parse(config); - const batteryVariables = await createProviderListener< - BatteryProviderConfig, - BatteryProvider - >(mergedmergedConfig, owner); + const { firstValue, onChange } = + await createProviderListener(mergedConfig); - return { - get chargePercent() { - return batteryVariables().chargePercent; - }, - get cycleCount() { - return batteryVariables().cycleCount; - }, - get healthPercent() { - return batteryVariables().healthPercent; - }, - get powerConsumption() { - return batteryVariables().powerConsumption; - }, - get state() { - return batteryVariables().state; - }, - get isCharging() { - return batteryVariables().isCharging; - }, - get timeTillEmpty() { - return batteryVariables().timeTillEmpty; - }, - get timeTillFull() { - return batteryVariables().timeTillFull; - }, - get voltage() { - return batteryVariables().voltage; - }, - }; + const batteryVariables = firstValue; + onChange(incoming => Object.assign(batteryVariables, incoming)); + + return batteryVariables; } diff --git a/packages/client-api/src/providers/cpu/create-cpu-provider.ts b/packages/client-api/src/providers/cpu/create-cpu-provider.ts index fd79462c..e82ce3b4 100644 --- a/packages/client-api/src/providers/cpu/create-cpu-provider.ts +++ b/packages/client-api/src/providers/cpu/create-cpu-provider.ts @@ -1,4 +1,3 @@ -import { type Owner } from 'solid-js'; import { z } from 'zod'; import { createProviderListener } from '../create-provider-listener'; @@ -25,32 +24,14 @@ export interface CpuProvider { vendor: string; } -export async function createCpuProvider( - config: CpuProviderConfig, - owner: Owner, -) { +export async function createCpuProvider(config: CpuProviderConfig) { const mergedConfig = CpuProviderConfigSchema.parse(config); - const cpuVariables = await createProviderListener< - CpuProviderConfig, - CpuProvider - >(mergedConfig, owner); - - return { - get frequency() { - return cpuVariables().frequency; - }, - get usage() { - return cpuVariables().usage; - }, - get logicalCoreCount() { - return cpuVariables().logicalCoreCount; - }, - get physicalCoreCount() { - return cpuVariables().physicalCoreCount; - }, - get vendor() { - return cpuVariables().vendor; - }, - }; + const { firstValue, onChange } = + await createProviderListener(mergedConfig); + + const cpuVariables = { ...firstValue, onChange }; + onChange(incoming => Object.assign(cpuVariables, incoming)); + + return cpuVariables; } diff --git a/packages/client-api/src/providers/create-provider.ts b/packages/client-api/src/providers/create-provider.ts index 5a756177..4483efcb 100644 --- a/packages/client-api/src/providers/create-provider.ts +++ b/packages/client-api/src/providers/create-provider.ts @@ -1,5 +1,3 @@ -import type { Owner } from 'solid-js'; - import { createBatteryProvider, type BatteryProviderConfig, @@ -8,70 +6,68 @@ import { createCpuProvider, type CpuProviderConfig, } from './cpu/create-cpu-provider'; -import { - createDateProvider, - type DateProviderConfig, -} from './date/create-date-provider'; -import { - createGlazeWmProvider, - type GlazeWmProviderConfig, -} from './glazewm/create-glazewm-provider'; -import { - createHostProvider, - type HostProviderConfig, -} from './host/create-host-provider'; -import { - createIpProvider, - type IpProviderConfig, -} from './ip/create-ip-provider'; -import { - createKomorebiProvider, - type KomorebiProviderConfig, -} from './komorebi/create-komorebi-provider'; -import { - createMemoryProvider, - type MemoryProviderConfig, -} from './memory/create-memory-provider'; -import { - createNetworkProvider, - type NetworkProviderConfig, -} from './network/create-network-provider'; -import { - createUtilProvider, - type UtilProviderConfig, -} from './util/create-util-provider'; -import { - createWeatherProvider, - type WeatherProviderConfig, -} from './weather/create-weather-provider'; +// import { +// createDateProvider, +// type DateProviderConfig, +// } from './date/create-date-provider'; +// import { +// createGlazeWmProvider, +// type GlazeWmProviderConfig, +// } from './glazewm/create-glazewm-provider'; +// import { +// createHostProvider, +// type HostProviderConfig, +// } from './host/create-host-provider'; +// import { +// createIpProvider, +// type IpProviderConfig, +// } from './ip/create-ip-provider'; +// import { +// createKomorebiProvider, +// type KomorebiProviderConfig, +// } from './komorebi/create-komorebi-provider'; +// import { +// createMemoryProvider, +// type MemoryProviderConfig, +// } from './memory/create-memory-provider'; +// import { +// createNetworkProvider, +// type NetworkProviderConfig, +// } from './network/create-network-provider'; +// import { +// createUtilProvider, +// type UtilProviderConfig, +// } from './util/create-util-provider'; +// import { +// createWeatherProvider, +// type WeatherProviderConfig, +// } from './weather/create-weather-provider'; -export type ProviderConfig = - | BatteryProviderConfig - | CpuProviderConfig - | DateProviderConfig - | GlazeWmProviderConfig - | HostProviderConfig - | IpProviderConfig - | KomorebiProviderConfig - | MemoryProviderConfig - | NetworkProviderConfig - | UtilProviderConfig - | WeatherProviderConfig; +export type ProviderConfig = BatteryProviderConfig | CpuProviderConfig; +// | DateProviderConfig +// | GlazeWmProviderConfig +// | HostProviderConfig +// | IpProviderConfig +// | KomorebiProviderConfig +// | MemoryProviderConfig +// | NetworkProviderConfig +// | UtilProviderConfig +// | WeatherProviderConfig; export type ProviderType = ProviderConfig['type']; const createProviderMap = { battery: createBatteryProvider, cpu: createCpuProvider, - date: createDateProvider, - glazewm: createGlazeWmProvider, - host: createHostProvider, - ip: createIpProvider, - komorebi: createKomorebiProvider, - memory: createMemoryProvider, - network: createNetworkProvider, - util: createUtilProvider, - weather: createWeatherProvider, + // date: createDateProvider, + // glazewm: createGlazeWmProvider, + // host: createHostProvider, + // ip: createIpProvider, + // komorebi: createKomorebiProvider, + // memory: createMemoryProvider, + // network: createNetworkProvider, + // util: createUtilProvider, + // weather: createWeatherProvider, } as const; type ProviderMap = typeof createProviderMap; @@ -87,7 +83,6 @@ export type ProviderOutput = ReturnType< export function createProvider( config: T, - owner: Owner, ): ProviderOutput { const providerFn = createProviderMap[config.type]; @@ -95,5 +90,5 @@ export function createProvider( throw new Error('Not a supported provider type.'); } - return providerFn(config as any, owner) as ProviderOutput; + return providerFn(config as any) as ProviderOutput; } diff --git a/packages/client-api/src/providers/index.ts b/packages/client-api/src/providers/index.ts index c51d6547..9be9ae01 100644 --- a/packages/client-api/src/providers/index.ts +++ b/packages/client-api/src/providers/index.ts @@ -1,11 +1,11 @@ -export * from './battery/create-battery-provider'; -export * from './cpu/create-cpu-provider'; -export * from './date/create-date-provider'; -export * from './glazewm/create-glazewm-provider'; -export * from './ip/create-ip-provider'; -export * from './memory/create-memory-provider'; -export * from './network/create-network-provider'; -export * from './util/create-util-provider'; -export * from './weather/create-weather-provider'; +// export * from './battery/create-battery-provider'; +// export * from './cpu/create-cpu-provider'; +// export * from './date/create-date-provider'; +// export * from './glazewm/create-glazewm-provider'; +// export * from './ip/create-ip-provider'; +// export * from './memory/create-memory-provider'; +// export * from './network/create-network-provider'; +// export * from './util/create-util-provider'; +// export * from './weather/create-weather-provider'; export * from './create-provider-listener'; export * from './create-provider'; From 6bf5e7bb121275dcbdcb969464a5b6d34ef438d2 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Mon, 2 Sep 2024 15:21:33 +0800 Subject: [PATCH 066/138] feat: working solidjs-ts client --- examples/solidjs-ts/src/index.tsx | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/examples/solidjs-ts/src/index.tsx b/examples/solidjs-ts/src/index.tsx index 8c7eacd9..5c9d2c83 100644 --- a/examples/solidjs-ts/src/index.tsx +++ b/examples/solidjs-ts/src/index.tsx @@ -11,7 +11,7 @@ import { onMount, Suspense, } from 'solid-js'; -import { App2 } from './App2'; +// import { App2 } from './App2'; import { createStore } from 'solid-js/store'; const root = document.getElementById('root'); @@ -31,9 +31,9 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) { // render(() => , root!); // }); // render(() => , root!); -init({}, async ctx => { - render(() => , root!); -}); +// init({}, async ctx => { +// render(() => , root!); +// }); // function App() { // const [context, setContext] = createSignal(); @@ -104,12 +104,14 @@ init({}, async ctx => { const ctx = await init(); -const [memory, cpu, glazewm, weather] = await ctx.createProviders([ - { type: 'memory' }, - { type: 'cpu' }, - { type: 'glazewm' }, - { type: 'weather' }, -]); +// const [memory, cpu, glazewm, weather] = await ctx.createProviders([ +// { type: 'memory' }, +// { type: 'cpu' }, +// { type: 'glazewm' }, +// { type: 'weather' }, +// ]); +// const battery = await ctx.createProvider({ type: 'battery' }); +const cpu = await ctx.createProvider({ type: 'cpu' }); render(() => , root!); @@ -125,16 +127,13 @@ function App() { // weather.onChange(setWeather); const [providers, setProviders] = createStore({ - memory, + // battery, cpu, - glazewm, - weather, }); - memory.onChange(memory => setProviders({ memory })); + // battery.onChange(battery => setProviders({ battery })); + //@ts-ignore cpu.onChange(cpu => setProviders({ cpu })); - glazewm.onChange(glazewm => setProviders({ glazewm })); - weather.onChange(weather => setProviders({ weather })); return
Hello World {providers.cpu.usage}
; } From 958942e8a6a4c85de8d196ec456906b843e4e0db Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Mon, 2 Sep 2024 15:39:17 +0800 Subject: [PATCH 067/138] feat: remove `client` package --- .vscode/launch.json | 8 +- .vscode/tasks.json | 37 --- CONTRIBUTING.md | 3 +- packages/client/README.md | 34 --- packages/client/index.html | 21 -- packages/client/package.json | 23 -- packages/client/src/index.tsx | 4 - packages/client/tsconfig.json | 11 - packages/client/vite.config.ts | 37 --- packages/desktop/package.json | 1 - packages/desktop/tauri.conf.json | 4 +- pnpm-lock.yaml | 476 +------------------------------ 12 files changed, 8 insertions(+), 651 deletions(-) delete mode 100644 .vscode/tasks.json delete mode 100644 packages/client/README.md delete mode 100644 packages/client/index.html delete mode 100644 packages/client/package.json delete mode 100644 packages/client/src/index.tsx delete mode 100644 packages/client/tsconfig.json delete mode 100644 packages/client/vite.config.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index 1a90431a..8a18be04 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,9 +11,7 @@ "--manifest-path=./packages/desktop/Cargo.toml", "--no-default-features" ] - }, - // Task for the `beforeDevCommand`, if used. - "preLaunchTask": "ui:dev" + } }, { "type": "lldb", @@ -25,9 +23,7 @@ "--release", "--manifest-path=./packages/desktop/Cargo.toml" ] - }, - // Task for the `beforeBuildCommand`, if used. - "preLaunchTask": "ui:build" + } } ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index d664da34..00000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "ui:dev", - "type": "shell", - // `dev` keeps running in the background. - "isBackground": true, - "command": "pnpm", - "args": ["dev"], - "problemMatcher": { - "owner": "typescript", - "source": "ts", - "applyTo": "closedDocuments", - "fileLocation": ["relative", "${cwd}"], - "pattern": "$tsc", - "background": { - "activeOnStart": true, - "beginsPattern": "^.*", - "endsPattern": "^.*Compiled successfully.*" - } - }, - "options": { - "cwd": "${workspaceRoot}/packages/client" - } - }, - { - "label": "ui:build", - "type": "shell", - "command": "pnpm", - "args": ["build"], - "options": { - "cwd": "${workspaceRoot}/packages/client" - } - } - ] -} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c3ca2d84..828416fc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,10 +31,9 @@ pnpm dev ## Architecture -Zebar is split into 3 packages: +Zebar is split into 2 packages: - `desktop` - a Tauri app which is a CLI that can spawn windows. -- `client` - a SolidJS frontend which is spawned by Tauri on `zebar open `. - `client-api` - business logic for communicating with Tauri. ### How to create a new provider? diff --git a/packages/client/README.md b/packages/client/README.md deleted file mode 100644 index 434f7bb9..00000000 --- a/packages/client/README.md +++ /dev/null @@ -1,34 +0,0 @@ -## Usage - -Those templates dependencies are maintained via [pnpm](https://pnpm.io) via `pnpm up -Lri`. - -This is the reason you see a `pnpm-lock.yaml`. That being said, any package manager will work. This file can be safely be removed once you clone a template. - -```bash -$ npm install # or pnpm install or yarn install -``` - -### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs) - -## Available Scripts - -In the project directory, you can run: - -### `npm dev` or `npm start` - -Runs the app in the development mode.
-Open [http://localhost:3000](http://localhost:3000) to view it in the browser. - -The page will reload if you make edits.
- -### `npm run build` - -Builds the app for production to the `dist` folder.
-It correctly bundles Solid in production mode and optimizes the build for the best performance. - -The build is minified and the filenames include the hashes.
-Your app is ready to be deployed! - -## Deployment - -You can deploy the `dist` folder to any static host provider (netlify, surge, now, etc.) diff --git a/packages/client/index.html b/packages/client/index.html deleted file mode 100644 index b12521f4..00000000 --- a/packages/client/index.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - diff --git a/packages/client/package.json b/packages/client/package.json deleted file mode 100644 index 7a6ef49f..00000000 --- a/packages/client/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "@zebar/client", - "version": "0.0.0", - "description": "", - "scripts": { - "build": "vite build --mode production", - "dev": "npm run wait:client-api && vite --mode development", - "wait:client-api": "wait-on ../client-api/dist/index.d.ts" - }, - "dependencies": { - "morphdom": "2.7.4", - "solid-js": "1.8.14", - "zebar": "workspace:*" - }, - "devDependencies": { - "@types/node": "20.11.17", - "typescript": "5.3.3", - "vite": "5.1.1", - "vite-plugin-checker": "0.6.4", - "vite-plugin-solid": "2.9.1", - "wait-on": "7.2.0" - } -} diff --git a/packages/client/src/index.tsx b/packages/client/src/index.tsx deleted file mode 100644 index ca6648d3..00000000 --- a/packages/client/src/index.tsx +++ /dev/null @@ -1,4 +0,0 @@ -/* @refresh reload */ -import { init } from 'zebar'; - -init({ includeDefaultCss: true }); diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json deleted file mode 100644 index 23a25d53..00000000 --- a/packages/client/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "@glzr/style-guide/tsconfig/solidjs-app", - "compilerOptions": { - "outDir": "dist", - "types": ["vite/client"], - "paths": { - "~/*": ["./src/app/*"] - } - }, - "include": ["src"] -} diff --git a/packages/client/vite.config.ts b/packages/client/vite.config.ts deleted file mode 100644 index 924738c2..00000000 --- a/packages/client/vite.config.ts +++ /dev/null @@ -1,37 +0,0 @@ -import path from 'path'; -import { defineConfig } from 'vite'; -import tsChecker from 'vite-plugin-checker'; -import solidPlugin from 'vite-plugin-solid'; - -export default defineConfig({ - plugins: [solidPlugin(), tsChecker({ typescript: true })], - // Prevent vite from obscuring Rust errors. - clearScreen: false, - // Tauri expects a fixed port. Fail if that port is not available. - server: { - port: 4200, - strictPort: true, - }, - // Allow use of `TAURI_DEBUG` and other env variables. - // Ref: https://tauri.studio/v1/api/config#buildconfig.beforedevcommand - envPrefix: ['VITE_', 'TAURI_'], - build: { - // Tauri supports ES2021. - target: - process.env.TAURI_PLATFORM == 'windows' ? 'chrome105' : 'safari13', - // Don't minify for debug builds. - minify: !process.env.TAURI_DEBUG ? 'esbuild' : false, - // Produce sourcemaps for debug builds. - sourcemap: !!process.env.TAURI_DEBUG, - }, - resolve: { - alias: { - '~': path.resolve(__dirname, './src/app'), - }, - }, - css: { - modules: { - localsConvention: 'dashes', - }, - }, -}); diff --git a/packages/desktop/package.json b/packages/desktop/package.json index fe9df5dc..3bbd885c 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -11,7 +11,6 @@ "dependencies": {}, "devDependencies": { "@tauri-apps/cli": "2.0.0-beta.22", - "@zebar/client": "workspace:*", "typescript": "5.3.3" } } diff --git a/packages/desktop/tauri.conf.json b/packages/desktop/tauri.conf.json index dd105b21..b8318583 100644 --- a/packages/desktop/tauri.conf.json +++ b/packages/desktop/tauri.conf.json @@ -1,8 +1,8 @@ { "$schema": "node_modules/@tauri-apps/cli/schema.json", "build": { - "devUrl": "http://localhost:4200", - "frontendDist": "../client/dist" + "devUrl": null, + "frontendDist": null }, "productName": "Zebar", "version": "0.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff6acbd3..3c9a7daf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,37 +34,6 @@ importers: specifier: 2.8.2 version: 2.8.2(solid-js@1.8.11)(vite@5.0.11(@types/node@20.11.17)(sass@1.70.0)) - packages/client: - dependencies: - morphdom: - specifier: 2.7.4 - version: 2.7.4 - solid-js: - specifier: 1.8.14 - version: 1.8.14 - zebar: - specifier: workspace:* - version: link:../client-api - devDependencies: - '@types/node': - specifier: 20.11.17 - version: 20.11.17 - typescript: - specifier: 5.3.3 - version: 5.3.3 - vite: - specifier: 5.1.1 - version: 5.1.1(@types/node@20.11.17)(sass@1.70.0) - vite-plugin-checker: - specifier: 0.6.4 - version: 0.6.4(typescript@5.3.3)(vite@5.1.1(@types/node@20.11.17)(sass@1.70.0)) - vite-plugin-solid: - specifier: 2.9.1 - version: 2.9.1(solid-js@1.8.14)(vite@5.1.1(@types/node@20.11.17)(sass@1.70.0)) - wait-on: - specifier: 7.2.0 - version: 7.2.0 - packages/client-api: dependencies: '@tauri-apps/api': @@ -110,9 +79,6 @@ importers: '@tauri-apps/cli': specifier: 2.0.0-beta.22 version: 2.0.0-beta.22 - '@zebar/client': - specifier: workspace:* - version: link:../client typescript: specifier: 5.3.3 version: 5.3.3 @@ -567,12 +533,6 @@ packages: typescript: optional: true - '@hapi/hoek@9.3.0': - resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} - - '@hapi/topo@5.1.0': - resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} - '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -680,15 +640,6 @@ packages: cpu: [x64] os: [win32] - '@sideway/address@4.1.4': - resolution: {integrity: sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==} - - '@sideway/formula@3.0.1': - resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==} - - '@sideway/pinpoint@2.0.0': - resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} - '@tauri-apps/api@2.0.0-beta.15': resolution: {integrity: sha512-H9w6iISmR+NvH4XuyCZB4zDN10tf9RFt6i/9JHEjaRhAowdAaJ+oiXq/3kedizNClHMtbTQ5j0oqDVPkZDAI8g==} engines: {node: '>= 18.18', npm: '>= 6.6.0', yarn: '>= 1.19.1'} @@ -785,10 +736,6 @@ packages: '@types/node@20.11.17': resolution: {integrity: sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==} - ansi-escapes@4.3.2: - resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} - engines: {node: '>=8'} - ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -820,12 +767,6 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} - asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - - axios@1.6.7: - resolution: {integrity: sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==} - babel-plugin-jsx-dom-expressions@0.37.13: resolution: {integrity: sha512-oAEMMIgU0h1DmHn4ZDaBBFc08nsVJciLq9pF7g0ZdpeIDKfY4zXjXr8+/oBjKhXG8nyomhnTodPjeG+/ZXcWXQ==} peerDependencies: @@ -851,9 +792,6 @@ packages: resolution: {integrity: sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==} engines: {node: '>= 5.10.0'} - brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} @@ -887,10 +825,6 @@ packages: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - chokidar@3.5.3: resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} engines: {node: '>= 8.10.0'} @@ -908,21 +842,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} - commander@8.3.0: - resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} - engines: {node: '>= 12'} - - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -954,10 +877,6 @@ packages: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} engines: {node: '>=12'} - delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - detect-indent@7.0.1: resolution: {integrity: sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g==} engines: {node: '>=12.20'} @@ -1025,27 +944,10 @@ packages: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} - follow-redirects@1.15.5: - resolution: {integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - foreground-child@3.1.1: resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} engines: {node: '>=14'} - form-data@4.0.0: - resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} - engines: {node: '>= 6'} - - fs-extra@11.2.0: - resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} - engines: {node: '>=14.14'} - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1096,17 +998,10 @@ packages: resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - html-entities@2.3.3: resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} @@ -1187,9 +1082,6 @@ packages: resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} engines: {node: '>=14'} - joi@17.11.0: - resolution: {integrity: sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==} - joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -1207,9 +1099,6 @@ packages: engines: {node: '>=6'} hasBin: true - jsonfile@6.1.0: - resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} - lilconfig@3.0.0: resolution: {integrity: sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==} engines: {node: '>=14'} @@ -1224,9 +1113,6 @@ packages: lodash.sortby@4.7.0: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - lru-cache@10.1.0: resolution: {integrity: sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==} engines: {node: 14 || >=16.14} @@ -1234,10 +1120,6 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} - luxon@3.4.4: resolution: {integrity: sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==} engines: {node: '>=12'} @@ -1257,14 +1139,6 @@ packages: resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} engines: {node: '>=8.6'} - mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - - mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -1273,23 +1147,14 @@ packages: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - minimatch@9.0.3: resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} engines: {node: '>=16 || 14 >=14.17'} - minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - minipass@7.0.4: resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} engines: {node: '>=16 || 14 >=14.17'} - morphdom@2.7.4: - resolution: {integrity: sha512-ATTbWMgGa+FaMU3FhnFYB6WgulCqwf6opOll4CBzmVDTLvPMmUPrEv8CudmLPK0MESa64+6B89fWOxP3+YIlxQ==} - ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} @@ -1392,9 +1257,6 @@ packages: engines: {node: '>=14'} hasBin: true - proxy-from-env@1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1426,9 +1288,6 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - rxjs@7.8.1: - resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} - s.color@0.0.15: resolution: {integrity: sha512-AUNrbEUHeKY8XsYr/DYpl+qk5+aM+DChopnWOPEzn8YKzOhv4l2zH6LzZms3tOZP3wwdOyc0RmTciyi46HLIuA==} @@ -1444,11 +1303,6 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.5.4: - resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} - engines: {node: '>=10'} - hasBin: true - seroval-plugins@1.0.4: resolution: {integrity: sha512-DQ2IK6oQVvy8k+c2V5x5YCtUa/GGGsUwUBNN9UqohrZ0rWdUapBFpNMYP1bCyRHoxOJjdKGl+dieacFIpU/i1A==} engines: {node: '>=10'} @@ -1544,10 +1398,6 @@ packages: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - synckit@0.8.5: resolution: {integrity: sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==} engines: {node: ^14.18.0 || >=16.0.0} @@ -1559,9 +1409,6 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - tiny-invariant@1.3.1: - resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} - titleize@3.0.0: resolution: {integrity: sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==} engines: {node: '>=12'} @@ -1614,10 +1461,6 @@ packages: typescript: optional: true - type-fest@0.21.3: - resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} - engines: {node: '>=10'} - typescript@5.3.3: resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} engines: {node: '>=14.17'} @@ -1626,10 +1469,6 @@ packages: undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} - untildify@4.0.0: resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} engines: {node: '>=8'} @@ -1643,53 +1482,12 @@ packages: validate-html-nesting@1.2.2: resolution: {integrity: sha512-hGdgQozCsQJMyfK5urgFcWEqsSSrK63Awe0t/IMR0bZ0QMtnuaiHzThW81guu3qx9abLi99NEuiaN6P9gVYsNg==} - vite-plugin-checker@0.6.4: - resolution: {integrity: sha512-2zKHH5oxr+ye43nReRbC2fny1nyARwhxdm0uNYp/ERy4YvU9iZpNOsueoi/luXw5gnpqRSvjcEPxXbS153O2wA==} - engines: {node: '>=14.16'} - peerDependencies: - eslint: '>=7' - meow: ^9.0.0 - optionator: ^0.9.1 - stylelint: '>=13' - typescript: '*' - vite: '>=2.0.0' - vls: '*' - vti: '*' - vue-tsc: '>=1.3.9' - peerDependenciesMeta: - eslint: - optional: true - meow: - optional: true - optionator: - optional: true - stylelint: - optional: true - typescript: - optional: true - vls: - optional: true - vti: - optional: true - vue-tsc: - optional: true - vite-plugin-solid@2.8.2: resolution: {integrity: sha512-HcvMs6DTxBaO4kE3psnirPQBCUUdYeQkCNKuB2TpEkJsxb6BGP6/7qkbbCSMxn25PyNdjvzVi1WXi0ou8KPgHw==} peerDependencies: solid-js: ^1.7.2 vite: ^3.0.0 || ^4.0.0 || ^5.0.0 - vite-plugin-solid@2.9.1: - resolution: {integrity: sha512-RC4hj+lbvljw57BbMGDApvEOPEh14lwrr/GeXRLNQLcR1qnOdzOwwTSFy13Gj/6FNIZpBEl0bWPU+VYFawrqUw==} - peerDependencies: - '@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.* - solid-js: ^1.7.2 - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 - peerDependenciesMeta: - '@testing-library/jest-dom': - optional: true - vite@5.0.11: resolution: {integrity: sha512-XBMnDjZcNAw/G1gEiskiM1v6yzM4GE5aMGvhWTlHAYYhxb7S3/V1s3m2LDHa8Vh6yIWYYB0iJwsEaS523c4oYA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -1718,34 +1516,6 @@ packages: terser: optional: true - vite@5.1.1: - resolution: {integrity: sha512-wclpAgY3F1tR7t9LL5CcHC41YPkQIpKUGeIuT8MdNwNZr6OqOTLs7JX5vIHAtzqLWXts0T+GDrh9pN2arneKqg==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - vitefu@0.2.5: resolution: {integrity: sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==} peerDependencies: @@ -1754,35 +1524,6 @@ packages: vite: optional: true - vscode-jsonrpc@6.0.0: - resolution: {integrity: sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==} - engines: {node: '>=8.0.0 || >=10.0.0'} - - vscode-languageclient@7.0.0: - resolution: {integrity: sha512-P9AXdAPlsCgslpP9pRxYPqkNYV7Xq8300/aZDpO35j1fJm/ncize8iGswzYlcvFw5DQUx4eVk+KvfXdL0rehNg==} - engines: {vscode: ^1.52.0} - - vscode-languageserver-protocol@3.16.0: - resolution: {integrity: sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==} - - vscode-languageserver-textdocument@1.0.11: - resolution: {integrity: sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==} - - vscode-languageserver-types@3.16.0: - resolution: {integrity: sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==} - - vscode-languageserver@7.0.0: - resolution: {integrity: sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==} - hasBin: true - - vscode-uri@3.0.8: - resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} - - wait-on@7.2.0: - resolution: {integrity: sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==} - engines: {node: '>=12.0.0'} - hasBin: true - webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} @@ -1805,9 +1546,6 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yaml@2.3.4: resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==} engines: {node: '>= 14'} @@ -2171,12 +1909,6 @@ snapshots: prettier: 3.2.5 typescript: 5.3.3 - '@hapi/hoek@9.3.0': {} - - '@hapi/topo@5.1.0': - dependencies: - '@hapi/hoek': 9.3.0 - '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -2266,14 +1998,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.10.0': optional: true - '@sideway/address@4.1.4': - dependencies: - '@hapi/hoek': 9.3.0 - - '@sideway/formula@3.0.1': {} - - '@sideway/pinpoint@2.0.0': {} - '@tauri-apps/api@2.0.0-beta.15': {} '@tauri-apps/cli-darwin-arm64@2.0.0-beta.22': @@ -2355,10 +2079,7 @@ snapshots: '@types/node@20.11.17': dependencies: undici-types: 5.26.5 - - ansi-escapes@4.3.2: - dependencies: - type-fest: 0.21.3 + optional: true ansi-regex@5.0.1: {} @@ -2383,16 +2104,6 @@ snapshots: array-union@2.1.0: {} - asynckit@0.4.0: {} - - axios@1.6.7: - dependencies: - follow-redirects: 1.15.5 - form-data: 4.0.0 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - babel-plugin-jsx-dom-expressions@0.37.13(@babel/core@7.23.7): dependencies: '@babel/core': 7.23.7 @@ -2417,11 +2128,6 @@ snapshots: dependencies: big-integer: 1.6.52 - brace-expansion@1.1.11: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - brace-expansion@2.0.1: dependencies: balanced-match: 1.0.2 @@ -2456,11 +2162,6 @@ snapshots: escape-string-regexp: 1.0.5 supports-color: 5.5.0 - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - chokidar@3.5.3: dependencies: anymatch: 3.1.3 @@ -2485,16 +2186,8 @@ snapshots: color-name@1.1.4: {} - combined-stream@1.0.8: - dependencies: - delayed-stream: 1.0.0 - commander@4.1.1: {} - commander@8.3.0: {} - - concat-map@0.0.1: {} - convert-source-map@2.0.0: {} cross-spawn@7.0.3: @@ -2523,8 +2216,6 @@ snapshots: define-lazy-prop@3.0.0: {} - delayed-stream@1.0.0: {} - detect-indent@7.0.1: {} detect-newline@4.0.1: {} @@ -2647,25 +2338,11 @@ snapshots: dependencies: to-regex-range: 5.0.1 - follow-redirects@1.15.5: {} - foreground-child@3.1.1: dependencies: cross-spawn: 7.0.3 signal-exit: 4.1.0 - form-data@4.0.0: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - mime-types: 2.1.35 - - fs-extra@11.2.0: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.1.0 - universalify: 2.0.1 - fsevents@2.3.3: optional: true @@ -2712,12 +2389,8 @@ snapshots: merge2: 1.4.1 slash: 4.0.0 - graceful-fs@4.2.11: {} - has-flag@3.0.0: {} - has-flag@4.0.0: {} - html-entities@2.3.3: {} human-signals@2.1.0: {} @@ -2771,14 +2444,6 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 - joi@17.11.0: - dependencies: - '@hapi/hoek': 9.3.0 - '@hapi/topo': 5.1.0 - '@sideway/address': 4.1.4 - '@sideway/formula': 3.0.1 - '@sideway/pinpoint': 2.0.0 - joycon@3.1.1: {} js-tokens@4.0.0: {} @@ -2787,12 +2452,6 @@ snapshots: json5@2.2.3: {} - jsonfile@6.1.0: - dependencies: - universalify: 2.0.1 - optionalDependencies: - graceful-fs: 4.2.11 - lilconfig@3.0.0: {} lines-and-columns@1.2.4: {} @@ -2801,18 +2460,12 @@ snapshots: lodash.sortby@4.7.0: {} - lodash@4.17.21: {} - lru-cache@10.1.0: {} lru-cache@5.1.1: dependencies: yallist: 3.1.1 - lru-cache@6.0.0: - dependencies: - yallist: 4.0.0 - luxon@3.4.4: {} merge-anything@5.1.7: @@ -2828,30 +2481,16 @@ snapshots: braces: 3.0.2 picomatch: 2.3.1 - mime-db@1.52.0: {} - - mime-types@2.1.35: - dependencies: - mime-db: 1.52.0 - mimic-fn@2.1.0: {} mimic-fn@4.0.0: {} - minimatch@3.1.2: - dependencies: - brace-expansion: 1.1.11 - minimatch@9.0.3: dependencies: brace-expansion: 2.0.1 - minimist@1.2.8: {} - minipass@7.0.4: {} - morphdom@2.7.4: {} - ms@2.1.2: {} mz@2.7.0: @@ -2936,8 +2575,6 @@ snapshots: prettier@3.2.5: {} - proxy-from-env@1.1.0: {} - punycode@2.3.1: {} queue-microtask@1.2.3: {} @@ -2977,10 +2614,6 @@ snapshots: dependencies: queue-microtask: 1.2.3 - rxjs@7.8.1: - dependencies: - tslib: 2.6.2 - s.color@0.0.15: {} sass-formatter@0.7.8: @@ -2996,10 +2629,6 @@ snapshots: semver@6.3.1: {} - semver@7.5.4: - dependencies: - lru-cache: 6.0.0 - seroval-plugins@1.0.4(seroval@1.0.4): dependencies: seroval: 1.0.4 @@ -3039,13 +2668,6 @@ snapshots: '@babel/types': 7.23.6 solid-js: 1.8.11 - solid-refresh@0.6.3(solid-js@1.8.14): - dependencies: - '@babel/generator': 7.23.6 - '@babel/helper-module-imports': 7.22.15 - '@babel/types': 7.23.6 - solid-js: 1.8.14 - sort-object-keys@1.1.3: {} sort-package-json@2.5.1: @@ -3106,10 +2728,6 @@ snapshots: dependencies: has-flag: 3.0.0 - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - synckit@0.8.5: dependencies: '@pkgr/utils': 2.4.2 @@ -3123,8 +2741,6 @@ snapshots: dependencies: any-promise: 1.3.0 - tiny-invariant@1.3.1: {} - titleize@3.0.0: {} to-fast-properties@2.0.0: {} @@ -3177,13 +2793,10 @@ snapshots: - supports-color - ts-node - type-fest@0.21.3: {} - typescript@5.3.3: {} - undici-types@5.26.5: {} - - universalify@2.0.1: {} + undici-types@5.26.5: + optional: true untildify@4.0.0: {} @@ -3195,27 +2808,6 @@ snapshots: validate-html-nesting@1.2.2: {} - vite-plugin-checker@0.6.4(typescript@5.3.3)(vite@5.1.1(@types/node@20.11.17)(sass@1.70.0)): - dependencies: - '@babel/code-frame': 7.23.5 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - chokidar: 3.5.3 - commander: 8.3.0 - fast-glob: 3.3.2 - fs-extra: 11.2.0 - npm-run-path: 4.0.1 - semver: 7.5.4 - strip-ansi: 6.0.1 - tiny-invariant: 1.3.1 - vite: 5.1.1(@types/node@20.11.17)(sass@1.70.0) - vscode-languageclient: 7.0.0 - vscode-languageserver: 7.0.0 - vscode-languageserver-textdocument: 1.0.11 - vscode-uri: 3.0.8 - optionalDependencies: - typescript: 5.3.3 - vite-plugin-solid@2.8.2(solid-js@1.8.11)(vite@5.0.11(@types/node@20.11.17)(sass@1.70.0)): dependencies: '@babel/core': 7.23.7 @@ -3230,19 +2822,6 @@ snapshots: transitivePeerDependencies: - supports-color - vite-plugin-solid@2.9.1(solid-js@1.8.14)(vite@5.1.1(@types/node@20.11.17)(sass@1.70.0)): - dependencies: - '@babel/core': 7.23.7 - '@types/babel__core': 7.20.5 - babel-preset-solid: 1.8.9(@babel/core@7.23.7) - merge-anything: 5.1.7 - solid-js: 1.8.14 - solid-refresh: 0.6.3(solid-js@1.8.14) - vite: 5.1.1(@types/node@20.11.17)(sass@1.70.0) - vitefu: 0.2.5(vite@5.1.1(@types/node@20.11.17)(sass@1.70.0)) - transitivePeerDependencies: - - supports-color - vite@5.0.11(@types/node@20.11.17)(sass@1.70.0): dependencies: esbuild: 0.19.11 @@ -3253,57 +2832,10 @@ snapshots: fsevents: 2.3.3 sass: 1.70.0 - vite@5.1.1(@types/node@20.11.17)(sass@1.70.0): - dependencies: - esbuild: 0.19.11 - postcss: 8.4.35 - rollup: 4.10.0 - optionalDependencies: - '@types/node': 20.11.17 - fsevents: 2.3.3 - sass: 1.70.0 - vitefu@0.2.5(vite@5.0.11(@types/node@20.11.17)(sass@1.70.0)): optionalDependencies: vite: 5.0.11(@types/node@20.11.17)(sass@1.70.0) - vitefu@0.2.5(vite@5.1.1(@types/node@20.11.17)(sass@1.70.0)): - optionalDependencies: - vite: 5.1.1(@types/node@20.11.17)(sass@1.70.0) - - vscode-jsonrpc@6.0.0: {} - - vscode-languageclient@7.0.0: - dependencies: - minimatch: 3.1.2 - semver: 7.5.4 - vscode-languageserver-protocol: 3.16.0 - - vscode-languageserver-protocol@3.16.0: - dependencies: - vscode-jsonrpc: 6.0.0 - vscode-languageserver-types: 3.16.0 - - vscode-languageserver-textdocument@1.0.11: {} - - vscode-languageserver-types@3.16.0: {} - - vscode-languageserver@7.0.0: - dependencies: - vscode-languageserver-protocol: 3.16.0 - - vscode-uri@3.0.8: {} - - wait-on@7.2.0: - dependencies: - axios: 1.6.7 - joi: 17.11.0 - lodash: 4.17.21 - minimist: 1.2.8 - rxjs: 7.8.1 - transitivePeerDependencies: - - debug - webidl-conversions@4.0.2: {} whatwg-url@7.1.0: @@ -3330,8 +2862,6 @@ snapshots: yallist@3.1.1: {} - yallist@4.0.0: {} - yaml@2.3.4: {} zod@3.22.4: {} From 319e645abc7ec42eff3af055e67ff853d635f71f Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Mon, 2 Sep 2024 18:10:25 +0800 Subject: [PATCH 068/138] feat: wip implementation of generic `Provider` type --- .../battery/create-battery-provider.ts | 1 + .../src/providers/cpu/create-cpu-provider.ts | 15 ++++--- .../src/providers/create-provider.ts | 23 ++++++++++ .../providers/date/create-date-provider.ts | 43 ++++++------------- 4 files changed, 45 insertions(+), 37 deletions(-) diff --git a/packages/client-api/src/providers/battery/create-battery-provider.ts b/packages/client-api/src/providers/battery/create-battery-provider.ts index 9b6c2ff2..2aa1efae 100644 --- a/packages/client-api/src/providers/battery/create-battery-provider.ts +++ b/packages/client-api/src/providers/battery/create-battery-provider.ts @@ -26,6 +26,7 @@ export interface BatteryProvider { timeTillEmpty: number | null; timeTillFull: number | null; voltage: number | null; + onChange: (provider: BatteryProvider) => void; } export async function createBatteryProvider( diff --git a/packages/client-api/src/providers/cpu/create-cpu-provider.ts b/packages/client-api/src/providers/cpu/create-cpu-provider.ts index e82ce3b4..ab9a4487 100644 --- a/packages/client-api/src/providers/cpu/create-cpu-provider.ts +++ b/packages/client-api/src/providers/cpu/create-cpu-provider.ts @@ -11,7 +11,7 @@ export interface CpuProviderConfig { refreshInterval?: number; } -const CpuProviderConfigSchema = z.object({ +const cpuProviderConfigSchema = z.object({ type: z.literal('cpu'), refreshInterval: z.coerce.number().default(5 * 1000), }); @@ -22,16 +22,19 @@ export interface CpuProvider { logicalCoreCount: number; physicalCoreCount: number; vendor: string; + onChange: (callback: (provider: CpuProvider) => void) => void; } -export async function createCpuProvider(config: CpuProviderConfig) { - const mergedConfig = CpuProviderConfigSchema.parse(config); +export async function createCpuProvider( + config: CpuProviderConfig, +): Promise { + const mergedConfig = cpuProviderConfigSchema.parse(config); const { firstValue, onChange } = await createProviderListener(mergedConfig); - const cpuVariables = { ...firstValue, onChange }; - onChange(incoming => Object.assign(cpuVariables, incoming)); + const cpuProvider = { ...firstValue, onChange }; + onChange(incoming => Object.assign(cpuProvider, incoming)); - return cpuVariables; + return cpuProvider; } diff --git a/packages/client-api/src/providers/create-provider.ts b/packages/client-api/src/providers/create-provider.ts index 4483efcb..1bf17191 100644 --- a/packages/client-api/src/providers/create-provider.ts +++ b/packages/client-api/src/providers/create-provider.ts @@ -70,6 +70,29 @@ const createProviderMap = { // weather: createWeatherProvider, } as const; +export interface Provider { + /** + * Current value of the provider. + */ + val: T; + + /** + * Refresh the provider's value. + */ + refresh(): Promise; + + /** + * Stop the provider. + */ + shutdown(): Promise; + + /** + * Listen for changes to the provider's value. + * @param callback - Callback to run when the value changes. + */ + onChange(callback: (nextVal: T) => void): void; +} + type ProviderMap = typeof createProviderMap; /** diff --git a/packages/client-api/src/providers/date/create-date-provider.ts b/packages/client-api/src/providers/date/create-date-provider.ts index 899fd2a1..b581d509 100644 --- a/packages/client-api/src/providers/date/create-date-provider.ts +++ b/packages/client-api/src/providers/date/create-date-provider.ts @@ -1,6 +1,4 @@ import { DateTime } from 'luxon'; -import { type Owner, onCleanup, runWithOwner } from 'solid-js'; -import { createStore } from 'solid-js/store'; import { z } from 'zod'; export interface DateProviderConfig { @@ -70,56 +68,39 @@ export interface DateProvider { toFormat(now: number, format: string): string; } -type DateVariables = Omit; - export async function createDateProvider( config: DateProviderConfig, - owner: Owner, -) { +): Promise { const mergedConfig = DateProviderConfigSchema.parse(config); - const [dateVariables, setDateVariables] = - createStore(getDateVariables()); + let val = getDateValue(); const interval = setInterval( - () => setDateVariables(getDateVariables()), + () => (val = getDateValue()), mergedConfig.refreshInterval, ); - runWithOwner(owner, () => { - onCleanup(() => clearInterval(interval)); - }); - - function getDateVariables() { + function getDateValue() { const date = new Date(); return { new: date, now: date.getTime(), iso: date.toISOString(), + formatted: 'todo', }; } - function toFormat(now: number, format: string) { - let dateTime = DateTime.fromMillis(now); - - if (mergedConfig.timezone) { - dateTime = dateTime.setZone(mergedConfig.timezone); - } - - return dateTime.toFormat(format, { locale: mergedConfig.locale }); - } - return { - get new() { - return dateVariables.new; + val, + refresh(): DateProvider { + val = getDateValue(); }, - get now() { - return dateVariables.now; + shutdown() { + clearInterval(interval); }, - get iso() { - return dateVariables.iso; + onChange(cb) { + return dateProvider.onChange(cb); }, - toFormat, }; } From 45f50066b1fd4c2f156d7c6d5aedc19ceec3879b Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Mon, 2 Sep 2024 21:37:25 +0800 Subject: [PATCH 069/138] feat: add `createbaseProvider` function for handling provider emissions in a shared way --- examples/solidjs-ts/src/App.tsx | 42 ------ examples/solidjs-ts/src/App2.tsx | 106 -------------- .../src/{App.module.css => index.css} | 0 examples/solidjs-ts/src/index.tsx | 133 +----------------- .../src/providers/cpu/create-cpu-provider.ts | 23 ++- .../src/providers/create-base-provider.ts | 130 +++++++++++++++++ .../src/providers/create-provider.ts | 23 --- .../providers/date/create-date-provider.ts | 47 +++---- 8 files changed, 174 insertions(+), 330 deletions(-) delete mode 100644 examples/solidjs-ts/src/App.tsx delete mode 100644 examples/solidjs-ts/src/App2.tsx rename examples/solidjs-ts/src/{App.module.css => index.css} (100%) create mode 100644 packages/client-api/src/providers/create-base-provider.ts diff --git a/examples/solidjs-ts/src/App.tsx b/examples/solidjs-ts/src/App.tsx deleted file mode 100644 index d7ac8a17..00000000 --- a/examples/solidjs-ts/src/App.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { ZebarContext } from 'zebar'; - -import styles from './App.module.css'; -import { createEffect } from 'solid-js'; -import { createMutable } from 'solid-js/store'; - -export default async function App(props: { ctx: ZebarContext }) { - const cpu = await props.ctx.createProvider({ type: 'cpu' }); - const memory = await props.ctx.createProvider({ type: 'memory' }); - - // const cpu = createMutable( - // await props.ctx.createProvider({ type: 'cpu' }), - // ); - // const memory = createMutable( - // await props.ctx.createProvider({ type: 'memory' }), - // ); - - createEffect(() => { - console.log('client', cpu); - console.log('client', cpu.usage); - }); - - return ( -
-
-

{cpu.usage}

-

{memory.usage}

-

- Edit src/App.tsx and save to reload. -

- - Learn Solid - -
-
- ); -} diff --git a/examples/solidjs-ts/src/App2.tsx b/examples/solidjs-ts/src/App2.tsx deleted file mode 100644 index 60137481..00000000 --- a/examples/solidjs-ts/src/App2.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { ZebarContext } from 'zebar'; - -import styles from './App.module.css'; -import { - Component, - createEffect, - createResource, - onCleanup, - onMount, - Show, - Suspense, -} from 'solid-js'; -import { createMutable, createStore } from 'solid-js/store'; - -export function App2(props: { ctx: ZebarContext }) { - const [cpu] = createResource(() => - props.ctx.createProvider({ type: 'cpu' }), - ); - - const [memory] = createResource(() => - props.ctx.createProvider({ type: 'memory' }), - ); - - // const cpu = createMutable( - // await props.ctx.createProvider({ type: 'cpu' }), - // ); - // const memory = createMutable( - // await props.ctx.createProvider({ type: 'memory' }), - // ); - - // const [providers] = createResource(async () => { - // return { - // cpu: await props.ctx.createProvider({ type: 'cpu' }), - // memory: await props.ctx.createProvider({ type: 'memory' }), - // }; - // }); - - // createEffect(() => { - // console.log('client', providers()); - // console.log('client', providers()?.cpu); - // }); - - // const [providers, setProviders] = createStore([]); - - // onMount(async () => { - // setProviders( - // await Promise.all([ - // props.ctx.createProvider({ type: 'cpu' }), - // props.ctx.createProvider({ type: 'memory' }), - // ]), - // ); - // }); - - createEffect(() => { - console.log('client', cpu()); - console.log('client', cpu()?.usage); - }); - - onCleanup(() => { - console.log('cleanup'); - }); - - return ( -
- -

{cpu().usage}

-
-
- ); - // return ( - //
- //

{providers[0]?.usage}

- //

{providers[0]?.usage}

- //
- // // - // // {(providers) => { - // //
- // //

{cpu.usage}

- // //

{memory.usage}

- // //
; - // // }} - // //
- // ); - // const [providers, setProviders] = createStore({ - // cpu: null, - // memory: null, - // loaded: false, - // }); - - // onMount(async () => { - // setProviders({ - // loaded: true, - // cpu: await props.ctx.createProvider({ type: 'cpu' }), - // memory: await props.ctx.createProvider({ type: 'memory' }), - // }); - // }); - - // return ( - // - //
- //

{providers.cpu?.usage}

- //

{providers.memory?.usage}

- //
- //
- // ); -} diff --git a/examples/solidjs-ts/src/App.module.css b/examples/solidjs-ts/src/index.css similarity index 100% rename from examples/solidjs-ts/src/App.module.css rename to examples/solidjs-ts/src/index.css diff --git a/examples/solidjs-ts/src/index.tsx b/examples/solidjs-ts/src/index.tsx index 5c9d2c83..09bddcce 100644 --- a/examples/solidjs-ts/src/index.tsx +++ b/examples/solidjs-ts/src/index.tsx @@ -1,139 +1,20 @@ /* @refresh reload */ +import './index.css'; import { render } from 'solid-js/web'; -import { init, ZebarContext } from 'zebar'; - -// import { App } from './App'; -import { - createEffect, - createResource, - createSignal, - lazy, - onMount, - Suspense, -} from 'solid-js'; -// import { App2 } from './App2'; import { createStore } from 'solid-js/store'; - -const root = document.getElementById('root'); - -if (import.meta.env.DEV && !(root instanceof HTMLElement)) { - throw new Error( - 'Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?', - ); -} - -// const xx = lazy(() => import('./App')); - -// init({}, ctx => { -// // const app = await App({ ctx }); -// // render(() => app, root!); -// // render(() => , root!); -// render(() => , root!); -// }); -// render(() => , root!); -// init({}, async ctx => { -// render(() => , root!); -// }); - -// function App() { -// const [context, setContext] = createSignal(); - -// const [aa] = createResource( -// () => context(), -// async context => { -// return { -// cpu: await context.createProvider({ type: 'cpu' }), -// memory: await context.createProvider({ type: 'memory' }), -// }; -// }, -// ); - -// init({}, ctx => setContext(ctx)); - -// const [providers, setProviders] = createStore(); - -// createEffect(async () => { -// if (context()) { -// setProviders( -// // await Promise.all([ -// // props.ctx.createProvider({ type: 'cpu' }), -// // props.ctx.createProvider({ type: 'memory' }), -// // ]), -// { -// cpu: await context().createProvider({ type: 'cpu' }), -// memory: await context().createProvider({ type: 'memory' }), -// }, -// ); -// console.log('aaa', providers); -// } -// }); - -// // return
Hello World {aa()?.cpu?.usage}
; -// return
Hello World {providers?.cpu?.usage}
; -// } - -// const ctx = await init(); - -// const providers = await ctx.createProviders({ -// memory: { type: 'memory' }, -// cpu: { type: 'cpu' }, -// glazewm: { type: 'glazewm' }, -// weather: { type: 'weather' }, -// }); - -// const cpu = await ctx.createProvider({ type: 'cpu' }); - -// async function bootstrap() { -// const ctx = await init(); -// const cpu = await ctx.createProvider({ type: 'cpu' }); -// const memory = await ctx.createProvider({ type: 'memory' }); -// // const providers = { cpu, memory }; - -// render(() => , root!); -// } - -// function App(props: { -// ctx: ZebarContext; -// providers: { cpu: any; memory: any }; -// }) { -// const [providers, setProviders] = createStore(providers); -// providers.onChange(setProviders); - -// return
Hello World {providers.cpu.usage}
; -// } +import { init } from 'zebar'; const ctx = await init(); - -// const [memory, cpu, glazewm, weather] = await ctx.createProviders([ -// { type: 'memory' }, -// { type: 'cpu' }, -// { type: 'glazewm' }, -// { type: 'weather' }, -// ]); -// const battery = await ctx.createProvider({ type: 'battery' }); const cpu = await ctx.createProvider({ type: 'cpu' }); -render(() => , root!); +render(() => , document.getElementById('root')!); function App() { - // const [memory, setMemory] = createStore(memory); - // const [cpu, setCpu] = createStore(cpu); - // const [glazewm, setGlazewm] = createStore(glazewm); - // const [weather, setWeather] = createStore(weather); - - // memory.onChange(setMemory); - // cpu.onChange(setCpu); - // glazewm.onChange(setGlazewm); - // weather.onChange(setWeather); - - const [providers, setProviders] = createStore({ - // battery, - cpu, + const [store, setStore] = createStore({ + cpu: cpu.value, }); - // battery.onChange(battery => setProviders({ battery })); - //@ts-ignore - cpu.onChange(cpu => setProviders({ cpu })); + cpu.onValue(cpu => setStore({ cpu })); - return
Hello World {providers.cpu.usage}
; + return
Hello World {store.cpu.usage}
; } diff --git a/packages/client-api/src/providers/cpu/create-cpu-provider.ts b/packages/client-api/src/providers/cpu/create-cpu-provider.ts index ab9a4487..f3ca143b 100644 --- a/packages/client-api/src/providers/cpu/create-cpu-provider.ts +++ b/packages/client-api/src/providers/cpu/create-cpu-provider.ts @@ -1,6 +1,10 @@ import { z } from 'zod'; import { createProviderListener } from '../create-provider-listener'; +import { + createBaseProvider, + type Provider, +} from '../create-base-provider'; export interface CpuProviderConfig { type: 'cpu'; @@ -16,13 +20,14 @@ const cpuProviderConfigSchema = z.object({ refreshInterval: z.coerce.number().default(5 * 1000), }); -export interface CpuProvider { +export type CpuProvider = Provider; + +export interface CpuValues { frequency: number; usage: number; logicalCoreCount: number; physicalCoreCount: number; vendor: string; - onChange: (callback: (provider: CpuProvider) => void) => void; } export async function createCpuProvider( @@ -30,11 +35,15 @@ export async function createCpuProvider( ): Promise { const mergedConfig = cpuProviderConfigSchema.parse(config); - const { firstValue, onChange } = - await createProviderListener(mergedConfig); + return createBaseProvider(mergedConfig, async queue => { + const { firstValue, onChange, unlisten } = + await createProviderListener(mergedConfig); - const cpuProvider = { ...firstValue, onChange }; - onChange(incoming => Object.assign(cpuProvider, incoming)); + queue.value(firstValue); + onChange(val => queue.value(val)); - return cpuProvider; + return async () => { + await unlisten(); + }; + }); } diff --git a/packages/client-api/src/providers/create-base-provider.ts b/packages/client-api/src/providers/create-base-provider.ts new file mode 100644 index 00000000..b9197a19 --- /dev/null +++ b/packages/client-api/src/providers/create-base-provider.ts @@ -0,0 +1,130 @@ +import { Deferred } from '~/utils'; +import type { ProviderConfig } from './create-provider'; + +export interface Provider { + /** + * Current value of the provider. + */ + value?: TVal; + + /** + * Error message from the provider. + */ + error?: string; + + /** + * Whether the provider currently has an error. + */ + hasError: boolean; + + /** + * Config for the provider. + */ + config: TConfig; + + /** + * Restart the provider. + */ + restart(): Promise; + + /** + * Stops the provider. + */ + shutdown(): Promise; + + /** + * Listens for changes to the provider's value. + * @param callback - Callback to run when the value changes. + */ + onValue(callback: (nextVal: TVal) => void): void; + + /** + * Listens for errors from the provider. + * @param callback - Callback to run when an error is emitted. + */ + onError(callback: (error: string) => void): void; +} + +type UnlistenFn = () => Promise; + +type ProviderFetcher = (queue: { + value: (nextVal: T) => void; + error: (nextError: string) => void; +}) => UnlistenFn | Promise; + +export async function createBaseProvider< + TConfig extends ProviderConfig, + TVal, +>( + config: TConfig, + fetcher: ProviderFetcher, +): Promise> { + const firstEmit = new Deferred(); + + const valueListeners: ((val: TVal) => void)[] = []; + const errorListeners: ((error: string) => void)[] = []; + + let latestEmission = { + value: null as TVal | null, + error: null as string | null, + hasError: false, + }; + + const shutdown = fetcher({ + value: value => { + firstEmit.resolve(true); + + latestEmission = { + value, + error: null, + hasError: false, + }; + + valueListeners.forEach(listener => listener(value)); + }, + error: error => { + firstEmit.resolve(true); + latestEmission = { + value: null, + error, + hasError: false, + }; + + errorListeners.forEach(listener => listener(error)); + }, + }); + + // Wait for the first emit before returning the provider. + await firstEmit.promise; + + return { + // @ts-ignore - TODO + get value() { + return latestEmission.value; + }, + // @ts-ignore - TODO + get error() { + return latestEmission.error; + }, + get hasError() { + return latestEmission.hasError; + }, + config, + restart: async () => { + // TODO: Implement restart. + return null as any; + }, + shutdown: async () => { + valueListeners.length = 0; + errorListeners.length = 0; + const shutdownFn = await shutdown; + await shutdownFn(); + }, + onValue: callback => { + valueListeners.push(callback); + }, + onError: callback => { + errorListeners.push(callback); + }, + }; +} diff --git a/packages/client-api/src/providers/create-provider.ts b/packages/client-api/src/providers/create-provider.ts index 1bf17191..4483efcb 100644 --- a/packages/client-api/src/providers/create-provider.ts +++ b/packages/client-api/src/providers/create-provider.ts @@ -70,29 +70,6 @@ const createProviderMap = { // weather: createWeatherProvider, } as const; -export interface Provider { - /** - * Current value of the provider. - */ - val: T; - - /** - * Refresh the provider's value. - */ - refresh(): Promise; - - /** - * Stop the provider. - */ - shutdown(): Promise; - - /** - * Listen for changes to the provider's value. - * @param callback - Callback to run when the value changes. - */ - onChange(callback: (nextVal: T) => void): void; -} - type ProviderMap = typeof createProviderMap; /** diff --git a/packages/client-api/src/providers/date/create-date-provider.ts b/packages/client-api/src/providers/date/create-date-provider.ts index b581d509..13a52945 100644 --- a/packages/client-api/src/providers/date/create-date-provider.ts +++ b/packages/client-api/src/providers/date/create-date-provider.ts @@ -1,6 +1,8 @@ import { DateTime } from 'luxon'; import { z } from 'zod'; +import { createBaseProvider } from '../create-base-provider'; + export interface DateProviderConfig { type: 'date'; @@ -73,34 +75,27 @@ export async function createDateProvider( ): Promise { const mergedConfig = DateProviderConfigSchema.parse(config); - let val = getDateValue(); + return createBaseProvider(mergedConfig, queue => { + queue.value(getDateValue()); - const interval = setInterval( - () => (val = getDateValue()), - mergedConfig.refreshInterval, - ); + const interval = setInterval( + () => queue.value(getDateValue()), + mergedConfig.refreshInterval, + ); - function getDateValue() { - const date = new Date(); + function getDateValue() { + const date = new Date(); - return { - new: date, - now: date.getTime(), - iso: date.toISOString(), - formatted: 'todo', - }; - } - - return { - val, - refresh(): DateProvider { - val = getDateValue(); - }, - shutdown() { + return { + new: date, + now: date.getTime(), + iso: date.toISOString(), + formatted: 'todo', + }; + } + + return () => { clearInterval(interval); - }, - onChange(cb) { - return dateProvider.onChange(cb); - }, - }; + }; + }); } From 39650c9d335998c7302918db53107b90fc8702f4 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Mon, 2 Sep 2024 21:42:46 +0800 Subject: [PATCH 070/138] feat: remove `util` provider; export as fn instead --- .../src/providers/create-provider.ts | 106 +++++++++--------- .../providers/util/create-util-provider.ts | 104 ----------------- .../client-api/src/utils/convert-bytes.ts | 76 +++++++++++++ packages/client-api/src/utils/index.ts | 2 +- .../client-api/src/utils/types/prettify.ts | 7 -- 5 files changed, 128 insertions(+), 167 deletions(-) delete mode 100644 packages/client-api/src/providers/util/create-util-provider.ts create mode 100644 packages/client-api/src/utils/convert-bytes.ts delete mode 100644 packages/client-api/src/utils/types/prettify.ts diff --git a/packages/client-api/src/providers/create-provider.ts b/packages/client-api/src/providers/create-provider.ts index 4483efcb..153baaed 100644 --- a/packages/client-api/src/providers/create-provider.ts +++ b/packages/client-api/src/providers/create-provider.ts @@ -6,68 +6,64 @@ import { createCpuProvider, type CpuProviderConfig, } from './cpu/create-cpu-provider'; -// import { -// createDateProvider, -// type DateProviderConfig, -// } from './date/create-date-provider'; -// import { -// createGlazeWmProvider, -// type GlazeWmProviderConfig, -// } from './glazewm/create-glazewm-provider'; -// import { -// createHostProvider, -// type HostProviderConfig, -// } from './host/create-host-provider'; -// import { -// createIpProvider, -// type IpProviderConfig, -// } from './ip/create-ip-provider'; -// import { -// createKomorebiProvider, -// type KomorebiProviderConfig, -// } from './komorebi/create-komorebi-provider'; -// import { -// createMemoryProvider, -// type MemoryProviderConfig, -// } from './memory/create-memory-provider'; -// import { -// createNetworkProvider, -// type NetworkProviderConfig, -// } from './network/create-network-provider'; -// import { -// createUtilProvider, -// type UtilProviderConfig, -// } from './util/create-util-provider'; -// import { -// createWeatherProvider, -// type WeatherProviderConfig, -// } from './weather/create-weather-provider'; +import { + createDateProvider, + type DateProviderConfig, +} from './date/create-date-provider'; +import { + createGlazeWmProvider, + type GlazeWmProviderConfig, +} from './glazewm/create-glazewm-provider'; +import { + createHostProvider, + type HostProviderConfig, +} from './host/create-host-provider'; +import { + createIpProvider, + type IpProviderConfig, +} from './ip/create-ip-provider'; +import { + createKomorebiProvider, + type KomorebiProviderConfig, +} from './komorebi/create-komorebi-provider'; +import { + createMemoryProvider, + type MemoryProviderConfig, +} from './memory/create-memory-provider'; +import { + createNetworkProvider, + type NetworkProviderConfig, +} from './network/create-network-provider'; +import { + createWeatherProvider, + type WeatherProviderConfig, +} from './weather/create-weather-provider'; -export type ProviderConfig = BatteryProviderConfig | CpuProviderConfig; -// | DateProviderConfig -// | GlazeWmProviderConfig -// | HostProviderConfig -// | IpProviderConfig -// | KomorebiProviderConfig -// | MemoryProviderConfig -// | NetworkProviderConfig -// | UtilProviderConfig -// | WeatherProviderConfig; +export type ProviderConfig = + | BatteryProviderConfig + | CpuProviderConfig + | DateProviderConfig + | GlazeWmProviderConfig + | HostProviderConfig + | IpProviderConfig + | KomorebiProviderConfig + | MemoryProviderConfig + | NetworkProviderConfig + | WeatherProviderConfig; export type ProviderType = ProviderConfig['type']; const createProviderMap = { battery: createBatteryProvider, cpu: createCpuProvider, - // date: createDateProvider, - // glazewm: createGlazeWmProvider, - // host: createHostProvider, - // ip: createIpProvider, - // komorebi: createKomorebiProvider, - // memory: createMemoryProvider, - // network: createNetworkProvider, - // util: createUtilProvider, - // weather: createWeatherProvider, + date: createDateProvider, + glazewm: createGlazeWmProvider, + host: createHostProvider, + ip: createIpProvider, + komorebi: createKomorebiProvider, + memory: createMemoryProvider, + network: createNetworkProvider, + weather: createWeatherProvider, } as const; type ProviderMap = typeof createProviderMap; diff --git a/packages/client-api/src/providers/util/create-util-provider.ts b/packages/client-api/src/providers/util/create-util-provider.ts deleted file mode 100644 index 18e46296..00000000 --- a/packages/client-api/src/providers/util/create-util-provider.ts +++ /dev/null @@ -1,104 +0,0 @@ -import type { Owner } from 'solid-js'; -import { z } from 'zod'; - -export interface UtilProviderConfig { - type: 'util'; -} - -const UtilProviderConfigSchema = z.object({ - type: z.literal('util'), -}); - -export interface UtilProvider { - convertBytes( - bytes: number, - decimals?: number, - unitType?: DataUnit, - ): string; -} - -export enum DataUnit { - BITS = 'bits', - SI_BYTES = 'si_bytes', - IEC_BYTES = 'iec_bytes', -} - -export async function createUtilProvider( - _: UtilProviderConfig, - __: Owner, -): Promise { - const bitUnits = ['b', 'Kb', 'Mb', 'Gb', 'Tb', 'Pb', 'Eb', 'Zb', 'Yb']; - - const byteCommonUnits = [ - 'B', - 'KB', - 'MB', - 'GB', - 'TB', - 'PB', - 'EB', - 'ZB', - 'YB', - ]; - - const byteIECUnits = [ - 'B', - 'KiB', - 'MiB', - 'GiB', - 'TiB', - 'PiB', - 'EiB', - 'ZiB', - 'YiB', - ]; - - function convertBytes( - bytes: number, - decimals: number = 0, - unitType: DataUnit = DataUnit.BITS, - ) { - let unitIndex = 1; // Kb/KB/KiB - - if (unitType === DataUnit.BITS) { - bytes *= 8; - return convert(1000, bitUnits, bytes, decimals, unitIndex); - } - - if (unitType === DataUnit.SI_BYTES) { - return convert(1000, byteCommonUnits, bytes, decimals, unitIndex); - } - - if (unitType === DataUnit.IEC_BYTES) { - return convert(1024, byteIECUnits, bytes, decimals, unitIndex); - } - - return 'NoUnit'; - } - - function convert( - k: number, - units: string[], - bytes: number, - decimals: number, - unitIndex: number, - ) { - const dm = decimals < 0 ? 0 : decimals; - - if (!+bytes) { - return `${(0.0).toFixed(dm)} ${units[unitIndex]}`; - } - - let i = Math.floor(Math.log(bytes) / Math.log(k)); - - if (i < unitIndex) { - i = unitIndex; - } - - return `${(bytes / Math.pow(k, i)).toFixed(dm)} ${units[i]?.trimStart()}`; - } - - return { - convertBytes, - }; -} diff --git a/packages/client-api/src/utils/convert-bytes.ts b/packages/client-api/src/utils/convert-bytes.ts new file mode 100644 index 00000000..f757b772 --- /dev/null +++ b/packages/client-api/src/utils/convert-bytes.ts @@ -0,0 +1,76 @@ +const bitUnits = ['b', 'Kb', 'Mb', 'Gb', 'Tb', 'Pb', 'Eb', 'Zb', 'Yb']; + +const byteCommonUnits = [ + 'B', + 'KB', + 'MB', + 'GB', + 'TB', + 'PB', + 'EB', + 'ZB', + 'YB', +]; + +const byteIECUnits = [ + 'B', + 'KiB', + 'MiB', + 'GiB', + 'TiB', + 'PiB', + 'EiB', + 'ZiB', + 'YiB', +]; + +export enum DataUnit { + BITS = 'bits', + SI_BYTES = 'si_bytes', + IEC_BYTES = 'iec_bytes', +} + +export function convertBytes( + bytes: number, + decimals: number = 0, + unitType: DataUnit = DataUnit.BITS, +) { + let unitIndex = 1; // Kb/KB/KiB + + if (unitType === DataUnit.BITS) { + bytes *= 8; + return convert(1000, bitUnits, bytes, decimals, unitIndex); + } + + if (unitType === DataUnit.SI_BYTES) { + return convert(1000, byteCommonUnits, bytes, decimals, unitIndex); + } + + if (unitType === DataUnit.IEC_BYTES) { + return convert(1024, byteIECUnits, bytes, decimals, unitIndex); + } + + return 'NoUnit'; +} + +function convert( + k: number, + units: string[], + bytes: number, + decimals: number, + unitIndex: number, +) { + const dm = decimals < 0 ? 0 : decimals; + + if (!+bytes) { + return `${(0.0).toFixed(dm)} ${units[unitIndex]}`; + } + + let i = Math.floor(Math.log(bytes) / Math.log(k)); + + if (i < unitIndex) { + i = unitIndex; + } + + return `${(bytes / Math.pow(k, i)).toFixed(dm)} ${units[i]?.trimStart()}`; +} diff --git a/packages/client-api/src/utils/index.ts b/packages/client-api/src/utils/index.ts index 9171e576..7f41fa57 100644 --- a/packages/client-api/src/utils/index.ts +++ b/packages/client-api/src/utils/index.ts @@ -1,4 +1,4 @@ -export * from './types/prettify'; +export * from './convert-bytes'; export * from './create-logger'; export * from './deferred'; export * from './get-coordinate-distance'; diff --git a/packages/client-api/src/utils/types/prettify.ts b/packages/client-api/src/utils/types/prettify.ts deleted file mode 100644 index 8d9ac734..00000000 --- a/packages/client-api/src/utils/types/prettify.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Utility type for expanding a type to include all its properties. - * Ref: https://youtube.com/shorts/2lCCKiWGlC0 - */ -export type Prettify = { - [K in keyof T]: T[K]; -} & {}; From 6376dc38a071e5051412b324c5e7ecb7e55acdcb Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Tue, 3 Sep 2024 15:18:55 +0800 Subject: [PATCH 071/138] feat: remove `createProviderListener`; modify `onProviderEmit` to be callable from `createBaseProvider` --- examples/solidjs-ts/src/index.tsx | 4 +- .../client-api/src/desktop/desktop-events.ts | 48 +++++++++++-------- .../battery/create-battery-provider.ts | 38 +++++++++------ .../src/providers/cpu/create-cpu-provider.ts | 22 ++++----- .../src/providers/create-base-provider.ts | 2 +- .../src/providers/create-provider-listener.ts | 47 ------------------ .../providers/date/create-date-provider.ts | 34 ++++++------- packages/client-api/src/providers/index.ts | 1 - 8 files changed, 82 insertions(+), 114 deletions(-) delete mode 100644 packages/client-api/src/providers/create-provider-listener.ts diff --git a/examples/solidjs-ts/src/index.tsx b/examples/solidjs-ts/src/index.tsx index 09bddcce..1bf94866 100644 --- a/examples/solidjs-ts/src/index.tsx +++ b/examples/solidjs-ts/src/index.tsx @@ -4,8 +4,8 @@ import { render } from 'solid-js/web'; import { createStore } from 'solid-js/store'; import { init } from 'zebar'; -const ctx = await init(); -const cpu = await ctx.createProvider({ type: 'cpu' }); +const zebarCtx = await init(); +const cpu = await zebarCtx.createProvider({ type: 'cpu' }); render(() => , document.getElementById('root')!); diff --git a/packages/client-api/src/desktop/desktop-events.ts b/packages/client-api/src/desktop/desktop-events.ts index 28c16169..e8585451 100644 --- a/packages/client-api/src/desktop/desktop-events.ts +++ b/packages/client-api/src/desktop/desktop-events.ts @@ -3,16 +3,13 @@ import { type Event, type UnlistenFn, } from '@tauri-apps/api/event'; +import type { ProviderConfig } from '~/providers'; -import { createLogger } from '~/utils'; +import { createLogger, simpleHash } from '~/utils'; +import { listenProvider, unlistenProvider } from './desktop-commands'; const logger = createLogger('desktop-events'); -export interface ProviderEmitEvent { - configHash: string; - variables: { data: T } | { error: string }; -} - let listenPromise: Promise | null = null; let callbacks: { @@ -20,26 +17,42 @@ let callbacks: { fn: (payload: Event>) => void; }[] = []; +export interface ProviderEmitEvent { + configHash: string; + variables: { data: T } | { error: string }; +} + /** * Listen for provider data. */ export async function onProviderEmit( - configHash: string, - callback: (payload: T) => void, -): Promise { + config: ProviderConfig, + callback: (event: ProviderEmitEvent) => void, +): Promise<() => Promise> { + const configHash = simpleHash(config); + registerEventCallback(configHash, callback); const unlisten = await (listenPromise ?? (listenPromise = listenProviderEmit())); - // Unlisten when there are no active callbacks. - return () => { + await listenProvider({ + configHash, + config, + trackedAccess: [], + }); + + return async () => { callbacks = callbacks.filter( callback => callback.configHash !== configHash, ); + await unlistenProvider(configHash); + + // Unlisten when there are no active callbacks. if (callbacks.length === 0) { unlisten(); + listenPromise = null; } }; } @@ -49,7 +62,7 @@ export async function onProviderEmit( */ function registerEventCallback( configHash: string, - callback: (payload: T) => void, + callback: (event: ProviderEmitEvent) => void, ) { const wrappedCallback = (event: Event>) => { // Ignore provider emissions for different configs. @@ -57,15 +70,8 @@ function registerEventCallback( return; } - const { variables } = event.payload; - - if ('error' in variables) { - logger.error('Incoming provider error:', variables.error); - throw new Error(variables.error); - } - - logger.debug('Incoming provider variables:', variables.data); - callback(variables.data as T); + logger.debug('Incoming provider emission:', event.payload); + callback(event.payload); }; callbacks.push({ configHash, fn: wrappedCallback }); diff --git a/packages/client-api/src/providers/battery/create-battery-provider.ts b/packages/client-api/src/providers/battery/create-battery-provider.ts index 2aa1efae..014772b6 100644 --- a/packages/client-api/src/providers/battery/create-battery-provider.ts +++ b/packages/client-api/src/providers/battery/create-battery-provider.ts @@ -1,6 +1,10 @@ import { z } from 'zod'; -import { createProviderListener } from '../create-provider-listener'; +import { + createBaseProvider, + type Provider, +} from '../create-base-provider'; +import { onProviderEmit } from '~/desktop'; export interface BatteryProviderConfig { type: 'battery'; @@ -11,12 +15,17 @@ export interface BatteryProviderConfig { refreshInterval?: number; } -const BatteryProviderConfigSchema = z.object({ +const batteryProviderConfigSchema = z.object({ type: z.literal('battery'), refreshInterval: z.coerce.number().default(60 * 60 * 1000), }); -export interface BatteryProvider { +export type BatteryProvider = Provider< + BatteryProviderConfig, + BatteryOutput +>; + +export interface BatteryOutput { chargePercent: number; cycleCount: number; healthPercent: number; @@ -26,19 +35,20 @@ export interface BatteryProvider { timeTillEmpty: number | null; timeTillFull: number | null; voltage: number | null; - onChange: (provider: BatteryProvider) => void; } export async function createBatteryProvider( config: BatteryProviderConfig, -) { - const mergedConfig = BatteryProviderConfigSchema.parse(config); - - const { firstValue, onChange } = - await createProviderListener(mergedConfig); - - const batteryVariables = firstValue; - onChange(incoming => Object.assign(batteryVariables, incoming)); - - return batteryVariables; +): Promise { + const mergedConfig = batteryProviderConfigSchema.parse(config); + + return createBaseProvider(mergedConfig, async queue => { + return onProviderEmit(mergedConfig, ({ variables }) => { + if ('error' in variables) { + queue.error(variables.error); + } else { + queue.value(variables.data); + } + }); + }); } diff --git a/packages/client-api/src/providers/cpu/create-cpu-provider.ts b/packages/client-api/src/providers/cpu/create-cpu-provider.ts index f3ca143b..5dbd3c3b 100644 --- a/packages/client-api/src/providers/cpu/create-cpu-provider.ts +++ b/packages/client-api/src/providers/cpu/create-cpu-provider.ts @@ -1,10 +1,10 @@ import { z } from 'zod'; -import { createProviderListener } from '../create-provider-listener'; import { createBaseProvider, type Provider, } from '../create-base-provider'; +import { onProviderEmit } from '~/desktop'; export interface CpuProviderConfig { type: 'cpu'; @@ -20,9 +20,9 @@ const cpuProviderConfigSchema = z.object({ refreshInterval: z.coerce.number().default(5 * 1000), }); -export type CpuProvider = Provider; +export type CpuProvider = Provider; -export interface CpuValues { +export interface CpuOutput { frequency: number; usage: number; logicalCoreCount: number; @@ -36,14 +36,12 @@ export async function createCpuProvider( const mergedConfig = cpuProviderConfigSchema.parse(config); return createBaseProvider(mergedConfig, async queue => { - const { firstValue, onChange, unlisten } = - await createProviderListener(mergedConfig); - - queue.value(firstValue); - onChange(val => queue.value(val)); - - return async () => { - await unlisten(); - }; + return onProviderEmit(mergedConfig, ({ variables }) => { + if ('error' in variables) { + queue.error(variables.error); + } else { + queue.value(variables.data); + } + }); }); } diff --git a/packages/client-api/src/providers/create-base-provider.ts b/packages/client-api/src/providers/create-base-provider.ts index b9197a19..d6527ffe 100644 --- a/packages/client-api/src/providers/create-base-provider.ts +++ b/packages/client-api/src/providers/create-base-provider.ts @@ -45,7 +45,7 @@ export interface Provider { onError(callback: (error: string) => void): void; } -type UnlistenFn = () => Promise; +type UnlistenFn = () => void | Promise; type ProviderFetcher = (queue: { value: (nextVal: T) => void; diff --git a/packages/client-api/src/providers/create-provider-listener.ts b/packages/client-api/src/providers/create-provider-listener.ts deleted file mode 100644 index ad08f2b4..00000000 --- a/packages/client-api/src/providers/create-provider-listener.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { - onProviderEmit, - listenProvider, - unlistenProvider, -} from '~/desktop'; -import { Deferred, simpleHash } from '~/utils'; -import type { ProviderConfig } from './create-provider'; - -export interface ProviderListener { - firstValue: TVars; - onChange: (callback: (val: TVars) => void) => void; - unlisten: () => Promise; -} - -/** - * Utility for listening to a provider of a given config type. - */ -export async function createProviderListener( - config: ProviderConfig, -): Promise> { - const configHash = simpleHash(config); - - const firstValue = new Deferred(); - const listeners: ((val: TVars) => void)[] = []; - - const unlistenEmit = await onProviderEmit(configHash, val => { - firstValue.resolve(val); - listeners.forEach(listener => listener(val)); - }); - - await listenProvider({ - configHash, - config, - trackedAccess: [], - }); - - return { - firstValue: await firstValue.promise, - onChange: callback => { - listeners.push(callback); - }, - unlisten: async () => { - unlistenEmit(); - await unlistenProvider(configHash); - }, - }; -} diff --git a/packages/client-api/src/providers/date/create-date-provider.ts b/packages/client-api/src/providers/date/create-date-provider.ts index 13a52945..2c819107 100644 --- a/packages/client-api/src/providers/date/create-date-provider.ts +++ b/packages/client-api/src/providers/date/create-date-provider.ts @@ -27,6 +27,18 @@ export interface DateProviderConfig { * A full list of ISO-639-1 locales can be found [here](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes#Table). */ locale?: string; + + /** + * Formatting of the current date into a custom string format. + * + * Refer to [table of tokens](https://moment.github.io/luxon/#/formatting?id=table-of-tokens) + * for available date/time tokens. + * + * @example + * "yyyy LLL dd" -> "2023 Feb 13" + * "HH 'hours and' mm 'minutes'" -> "20 hours and 55 minutes" + */ + formatting?: string; } const DateProviderConfigSchema = z.object({ @@ -36,7 +48,7 @@ const DateProviderConfigSchema = z.object({ locale: z.string().optional(), }); -export interface DateProvider { +export interface DateOutput { /** * Current date/time as a JavaScript `Date` object. Uses `new Date()` under * the hood. @@ -54,25 +66,15 @@ export interface DateProvider { * `2017-04-22T20:47:05.335-04:00`). Uses `date.toISOString()` under the hood. **/ iso: string; - - /** - * Format a given date/time into a custom string format. - * - * Refer to [table of tokens](https://moment.github.io/luxon/#/formatting?id=table-of-tokens) - * for available date/time tokens. - * - * @example - * `toFormat(now, 'yyyy LLL dd')` -> `2023 Feb 13` - * `toFormat(now, "HH 'hours and' mm 'minutes'")` -> `20 hours and 55 minutes` - * @param now Date/time as milliseconds since epoch. - * @param format Custom string format. - */ - toFormat(now: number, format: string): string; } +// TODO: Implement `createBaseProvider` for all providers. +// TODO: Remove `createProviderListener`. Instead move listen function to `desktop-events.ts`. +// TODO: Organize provider-related types. +// TODO: Remove `toFormat` on date provider. Instead add `formatting` to the config. export async function createDateProvider( config: DateProviderConfig, -): Promise { +): Promise { const mergedConfig = DateProviderConfigSchema.parse(config); return createBaseProvider(mergedConfig, queue => { diff --git a/packages/client-api/src/providers/index.ts b/packages/client-api/src/providers/index.ts index 9be9ae01..c84030a4 100644 --- a/packages/client-api/src/providers/index.ts +++ b/packages/client-api/src/providers/index.ts @@ -7,5 +7,4 @@ // export * from './network/create-network-provider'; // export * from './util/create-util-provider'; // export * from './weather/create-weather-provider'; -export * from './create-provider-listener'; export * from './create-provider'; From ef686d57209f67380f45c17b5f10ff1ed80971ed Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Tue, 3 Sep 2024 17:17:12 +0800 Subject: [PATCH 072/138] feat: update all providers to use `createBaseProvider` --- examples/solidjs-ts/src/index.tsx | 23 +- .../src/providers/create-base-provider.ts | 9 +- .../providers/date/create-date-provider.ts | 15 +- .../glazewm/create-glazewm-provider.ts | 307 ++++++++---------- .../providers/host/create-host-provider.ts | 54 ++- .../src/providers/ip/create-ip-provider.ts | 51 ++- .../komorebi/create-komorebi-provider.ts | 172 +++++----- .../memory/create-memory-provider.ts | 57 ++-- .../network/create-network-provider.ts | 49 ++- .../weather/create-weather-provider.ts | 73 ++--- 10 files changed, 373 insertions(+), 437 deletions(-) diff --git a/examples/solidjs-ts/src/index.tsx b/examples/solidjs-ts/src/index.tsx index 1bf94866..d7a8e6f8 100644 --- a/examples/solidjs-ts/src/index.tsx +++ b/examples/solidjs-ts/src/index.tsx @@ -5,16 +5,37 @@ import { createStore } from 'solid-js/store'; import { init } from 'zebar'; const zebarCtx = await init(); +const glazewm = await zebarCtx.createProvider({ type: 'glazewm' }); const cpu = await zebarCtx.createProvider({ type: 'cpu' }); +const battery = await zebarCtx.createProvider({ type: 'battery' }); +const memory = await zebarCtx.createProvider({ type: 'memory' }); +const weather = await zebarCtx.createProvider({ type: 'weather' }); render(() => , document.getElementById('root')!); function App() { const [store, setStore] = createStore({ + glazewm: glazewm.value, cpu: cpu.value, + battery: battery.value, + memory: memory.value, + weather: weather.value, }); + glazewm.onValue(glazewm => setStore({ glazewm })); cpu.onValue(cpu => setStore({ cpu })); + battery.onValue(battery => setStore({ battery })); + memory.onValue(memory => setStore({ memory })); + weather.onValue(weather => setStore({ weather })); - return
Hello World {store.cpu.usage}
; + return ( +
+ glazewm: {JSON.stringify(store.glazewm)} + cpu: {store.cpu.usage} + battery: {store.battery?.chargePercent} + memory: {store.memory.usage} + weather temp: {store.weather.celsiusTemp} + weather status: {store.weather.status} +
+ ); } diff --git a/packages/client-api/src/providers/create-base-provider.ts b/packages/client-api/src/providers/create-base-provider.ts index d6527ffe..a210d4c8 100644 --- a/packages/client-api/src/providers/create-base-provider.ts +++ b/packages/client-api/src/providers/create-base-provider.ts @@ -59,7 +59,7 @@ export async function createBaseProvider< config: TConfig, fetcher: ProviderFetcher, ): Promise> { - const firstEmit = new Deferred(); + const hasFirstEmit = new Deferred(); const valueListeners: ((val: TVal) => void)[] = []; const errorListeners: ((error: string) => void)[] = []; @@ -72,7 +72,7 @@ export async function createBaseProvider< const shutdown = fetcher({ value: value => { - firstEmit.resolve(true); + hasFirstEmit.resolve(true); latestEmission = { value, @@ -83,7 +83,8 @@ export async function createBaseProvider< valueListeners.forEach(listener => listener(value)); }, error: error => { - firstEmit.resolve(true); + hasFirstEmit.resolve(true); + latestEmission = { value: null, error, @@ -95,7 +96,7 @@ export async function createBaseProvider< }); // Wait for the first emit before returning the provider. - await firstEmit.promise; + await hasFirstEmit.promise; return { // @ts-ignore - TODO diff --git a/packages/client-api/src/providers/date/create-date-provider.ts b/packages/client-api/src/providers/date/create-date-provider.ts index 2c819107..eb9c0163 100644 --- a/packages/client-api/src/providers/date/create-date-provider.ts +++ b/packages/client-api/src/providers/date/create-date-provider.ts @@ -1,7 +1,10 @@ import { DateTime } from 'luxon'; import { z } from 'zod'; -import { createBaseProvider } from '../create-base-provider'; +import { + createBaseProvider, + type Provider, +} from '../create-base-provider'; export interface DateProviderConfig { type: 'date'; @@ -41,13 +44,15 @@ export interface DateProviderConfig { formatting?: string; } -const DateProviderConfigSchema = z.object({ +const dateProviderConfigSchema = z.object({ type: z.literal('date'), refreshInterval: z.coerce.number().default(1000), timezone: z.string().optional(), locale: z.string().optional(), }); +export type DateProvider = Provider; + export interface DateOutput { /** * Current date/time as a JavaScript `Date` object. Uses `new Date()` under @@ -68,14 +73,12 @@ export interface DateOutput { iso: string; } -// TODO: Implement `createBaseProvider` for all providers. -// TODO: Remove `createProviderListener`. Instead move listen function to `desktop-events.ts`. // TODO: Organize provider-related types. // TODO: Remove `toFormat` on date provider. Instead add `formatting` to the config. export async function createDateProvider( config: DateProviderConfig, -): Promise { - const mergedConfig = DateProviderConfigSchema.parse(config); +): Promise { + const mergedConfig = dateProviderConfigSchema.parse(config); return createBaseProvider(mergedConfig, queue => { queue.value(getDateValue()); diff --git a/packages/client-api/src/providers/glazewm/create-glazewm-provider.ts b/packages/client-api/src/providers/glazewm/create-glazewm-provider.ts index 62ed0cab..3f3d5a34 100644 --- a/packages/client-api/src/providers/glazewm/create-glazewm-provider.ts +++ b/packages/client-api/src/providers/glazewm/create-glazewm-provider.ts @@ -14,22 +14,29 @@ import { type WorkspaceDeactivatedEvent, type WorkspaceUpdatedEvent, } from 'glazewm'; -import { createEffect, on, runWithOwner, type Owner } from 'solid-js'; -import { createStore } from 'solid-js/store'; import { z } from 'zod'; import { getMonitors } from '~/desktop'; import { getCoordinateDistance } from '~/utils'; +import { + createBaseProvider, + type Provider, +} from '../create-base-provider'; export interface GlazeWmProviderConfig { type: 'glazewm'; } -const GlazeWmProviderConfigSchema = z.object({ +const glazeWmProviderConfigSchema = z.object({ type: z.literal('glazewm'), }); -export interface GlazeWmProvider { +export type GlazeWmProvider = Provider< + GlazeWmProviderConfig, + GlazeWmOutput +>; + +export interface GlazeWmOutput { /** * Workspace displayed on the current monitor. */ @@ -92,175 +99,149 @@ export interface GlazeWmProvider { } export async function createGlazeWmProvider( - _: GlazeWmProviderConfig, - owner: Owner, + config: GlazeWmProviderConfig, ): Promise { - const monitors = await getMonitors(); - const client = new WmClient(); - - const [glazeWmVariables, setGlazeWmVariables] = createStore( - await getInitialState(), - ); - - await client.subscribeMany( - [ - WmEventType.BINDING_MODES_CHANGED, - WmEventType.FOCUS_CHANGED, - WmEventType.FOCUSED_CONTAINER_MOVED, - WmEventType.TILING_DIRECTION_CHANGED, - WmEventType.WORKSPACE_ACTIVATED, - WmEventType.WORKSPACE_DEACTIVATED, - WmEventType.WORKSPACE_UPDATED, - ], - onEvent, - ); - - runWithOwner(owner, () => { - createEffect( - on( - () => monitors.currentMonitor, - async () => setGlazeWmVariables({ ...(await getMonitorState()) }), - ), + const mergedConfig = glazeWmProviderConfigSchema.parse(config); + + return createBaseProvider(mergedConfig, async queue => { + const monitors = await getMonitors(); + const client = new WmClient(); + + let state = await getInitialState(); + queue.value(state); + + const unlisten = await client.subscribeMany( + [ + WmEventType.BINDING_MODES_CHANGED, + WmEventType.FOCUS_CHANGED, + WmEventType.FOCUSED_CONTAINER_MOVED, + WmEventType.TILING_DIRECTION_CHANGED, + WmEventType.WORKSPACE_ACTIVATED, + WmEventType.WORKSPACE_DEACTIVATED, + WmEventType.WORKSPACE_UPDATED, + ], + onEvent, ); - }); - async function onEvent( - e: - | BindingModesChangedEvent - | FocusChangedEvent - | FocusedContainerMovedEvent - | TilingDirectionChangedEvent - | WorkspaceActivatedEvent - | WorkspaceDeactivatedEvent - | WorkspaceUpdatedEvent, - ) { - switch (e.eventType) { - case WmEventType.BINDING_MODES_CHANGED: { - setGlazeWmVariables({ bindingModes: e.newBindingModes }); - break; + // TODO: Update state when monitors change. + // monitors.onChange(async () => { + // state = { ...state, ...(await getMonitorState()) }; + // queue.value(state); + // }); + + async function onEvent( + e: + | BindingModesChangedEvent + | FocusChangedEvent + | FocusedContainerMovedEvent + | TilingDirectionChangedEvent + | WorkspaceActivatedEvent + | WorkspaceDeactivatedEvent + | WorkspaceUpdatedEvent, + ) { + switch (e.eventType) { + case WmEventType.BINDING_MODES_CHANGED: { + state = { ...state, bindingModes: e.newBindingModes }; + break; + } + case WmEventType.FOCUS_CHANGED: { + state = { ...state, focusedContainer: e.focusedContainer }; + state = { ...state, ...(await getMonitorState()) }; + + const { tilingDirection } = await client.queryTilingDirection(); + state = { ...state, tilingDirection }; + break; + } + case WmEventType.FOCUSED_CONTAINER_MOVED: { + state = { ...state, focusedContainer: e.focusedContainer }; + state = { ...state, ...(await getMonitorState()) }; + break; + } + case WmEventType.TILING_DIRECTION_CHANGED: { + state = { ...state, tilingDirection: e.newTilingDirection }; + break; + } + case WmEventType.WORKSPACE_ACTIVATED: + case WmEventType.WORKSPACE_DEACTIVATED: + case WmEventType.WORKSPACE_UPDATED: { + state = { ...state, ...(await getMonitorState()) }; + break; + } } - case WmEventType.FOCUS_CHANGED: { - setGlazeWmVariables({ focusedContainer: e.focusedContainer }); - setGlazeWmVariables({ ...(await getMonitorState()) }); - const { tilingDirection } = await client.queryTilingDirection(); - setGlazeWmVariables({ tilingDirection }); - break; - } - case WmEventType.FOCUSED_CONTAINER_MOVED: { - setGlazeWmVariables({ focusedContainer: e.focusedContainer }); - setGlazeWmVariables({ ...(await getMonitorState()) }); - break; - } - case WmEventType.TILING_DIRECTION_CHANGED: { - setGlazeWmVariables({ tilingDirection: e.newTilingDirection }); - break; - } - case WmEventType.WORKSPACE_ACTIVATED: - case WmEventType.WORKSPACE_DEACTIVATED: - case WmEventType.WORKSPACE_UPDATED: { - setGlazeWmVariables({ ...(await getMonitorState()) }); - break; - } + queue.value(state); } - } - - async function getInitialState() { - const { focused: focusedContainer } = await client.queryFocused(); - const { bindingModes } = await client.queryBindingModes(); - const { tilingDirection } = await client.queryTilingDirection(); - - return { - ...(await getMonitorState()), - focusedContainer, - tilingDirection, - bindingModes, - }; - } - - async function getMonitorState() { - const currentPosition = { - x: monitors.currentMonitor!.x, - y: monitors.currentMonitor!.y, - }; - - const { monitors: glazeWmMonitors } = await client.queryMonitors(); - // Get GlazeWM monitor that corresponds to the Zebar window's monitor. - const currentGlazeWmMonitor = glazeWmMonitors.reduce((a, b) => - getCoordinateDistance(currentPosition, a) < - getCoordinateDistance(currentPosition, b) - ? a - : b, - ); - - const focusedGlazeWmMonitor = glazeWmMonitors.find( - monitor => monitor.hasFocus, - ); + function focusWorkspace(name: string) { + client.runCommand(`focus --workspace ${name}`); + } - const allGlazeWmWorkspaces = glazeWmMonitors.flatMap( - monitor => monitor.children, - ); + function toggleTilingDirection() { + client.runCommand('toggle-tiling-direction'); + } - const focusedGlazeWmWorkspace = focusedGlazeWmMonitor?.children.find( - workspace => workspace.hasFocus, - ); + async function getInitialState() { + const { focused: focusedContainer } = await client.queryFocused(); + const { bindingModes } = await client.queryBindingModes(); + const { tilingDirection } = await client.queryTilingDirection(); + + return { + ...(await getMonitorState()), + focusedContainer, + tilingDirection, + bindingModes, + focusWorkspace, + toggleTilingDirection, + }; + } - const displayedGlazeWmWorkspace = currentGlazeWmMonitor.children.find( - workspace => workspace.isDisplayed, - ); + async function getMonitorState() { + const currentPosition = { + x: monitors.currentMonitor!.x, + y: monitors.currentMonitor!.y, + }; + + const { monitors: glazeWmMonitors } = await client.queryMonitors(); + + // Get GlazeWM monitor that corresponds to the Zebar window's monitor. + const currentGlazeWmMonitor = glazeWmMonitors.reduce((a, b) => + getCoordinateDistance(currentPosition, a) < + getCoordinateDistance(currentPosition, b) + ? a + : b, + ); + + const focusedGlazeWmMonitor = glazeWmMonitors.find( + monitor => monitor.hasFocus, + ); + + const allGlazeWmWorkspaces = glazeWmMonitors.flatMap( + monitor => monitor.children, + ); + + const focusedGlazeWmWorkspace = focusedGlazeWmMonitor?.children.find( + workspace => workspace.hasFocus, + ); + + const displayedGlazeWmWorkspace = + currentGlazeWmMonitor.children.find( + workspace => workspace.isDisplayed, + ); + + return { + displayedWorkspace: displayedGlazeWmWorkspace!, + focusedWorkspace: focusedGlazeWmWorkspace!, + currentWorkspaces: currentGlazeWmMonitor.children, + allWorkspaces: allGlazeWmWorkspaces, + focusedMonitor: focusedGlazeWmMonitor!, + currentMonitor: currentGlazeWmMonitor, + allMonitors: glazeWmMonitors, + }; + } - return { - displayedWorkspace: displayedGlazeWmWorkspace!, - focusedWorkspace: focusedGlazeWmWorkspace!, - currentWorkspaces: currentGlazeWmMonitor.children, - allWorkspaces: allGlazeWmWorkspaces, - focusedMonitor: focusedGlazeWmMonitor!, - currentMonitor: currentGlazeWmMonitor, - allMonitors: glazeWmMonitors, + return () => { + unlisten(); + client.closeConnection(); }; - } - - function focusWorkspace(name: string) { - client.runCommand(`focus --workspace ${name}`); - } - - function toggleTilingDirection() { - client.runCommand('toggle-tiling-direction'); - } - - return { - get displayedWorkspace() { - return glazeWmVariables.displayedWorkspace; - }, - get focusedWorkspace() { - return glazeWmVariables.focusedWorkspace; - }, - get currentWorkspaces() { - return glazeWmVariables.currentWorkspaces; - }, - get allWorkspaces() { - return glazeWmVariables.allWorkspaces; - }, - get allMonitors() { - return glazeWmVariables.allMonitors; - }, - get focusedMonitor() { - return glazeWmVariables.focusedMonitor; - }, - get currentMonitor() { - return glazeWmVariables.currentMonitor; - }, - get focusedContainer() { - return glazeWmVariables.focusedContainer; - }, - get tilingDirection() { - return glazeWmVariables.tilingDirection; - }, - get bindingModes() { - return glazeWmVariables.bindingModes; - }, - focusWorkspace, - toggleTilingDirection, - }; + }); } diff --git a/packages/client-api/src/providers/host/create-host-provider.ts b/packages/client-api/src/providers/host/create-host-provider.ts index 5ee00b0e..5bbd0550 100644 --- a/packages/client-api/src/providers/host/create-host-provider.ts +++ b/packages/client-api/src/providers/host/create-host-provider.ts @@ -1,7 +1,10 @@ -import type { Owner } from 'solid-js'; import { z } from 'zod'; -import { createProviderListener } from '../create-provider-listener'; +import { + createBaseProvider, + type Provider, +} from '../create-base-provider'; +import { onProviderEmit } from '~/desktop'; export interface HostProviderConfig { type: 'host'; @@ -12,12 +15,14 @@ export interface HostProviderConfig { refreshInterval?: number; } -const HostProviderConfigSchema = z.object({ +const hostProviderConfigSchema = z.object({ type: z.literal('host'), refreshInterval: z.coerce.number().default(60 * 1000), }); -export interface HostProvider { +export type HostProvider = Provider; + +export interface HostOutput { hostname: string | null; osName: string | null; osVersion: string | null; @@ -28,33 +33,16 @@ export interface HostProvider { export async function createHostProvider( config: HostProviderConfig, - owner: Owner, -) { - const mergedConfig = HostProviderConfigSchema.parse(config); - - const hostVariables = await createProviderListener< - HostProviderConfig, - HostProvider - >(mergedConfig, owner); - - return { - get hostname() { - return hostVariables().hostname; - }, - get osName() { - return hostVariables().osName; - }, - get osVersion() { - return hostVariables().osVersion; - }, - get friendlyOsVersion() { - return hostVariables().friendlyOsVersion; - }, - get bootTime() { - return hostVariables().bootTime; - }, - get uptime() { - return hostVariables().uptime; - }, - }; +): Promise { + const mergedConfig = hostProviderConfigSchema.parse(config); + + return createBaseProvider(mergedConfig, async queue => { + return onProviderEmit(mergedConfig, ({ variables }) => { + if ('error' in variables) { + queue.error(variables.error); + } else { + queue.value(variables.data); + } + }); + }); } diff --git a/packages/client-api/src/providers/ip/create-ip-provider.ts b/packages/client-api/src/providers/ip/create-ip-provider.ts index d79d1faa..888a9b21 100644 --- a/packages/client-api/src/providers/ip/create-ip-provider.ts +++ b/packages/client-api/src/providers/ip/create-ip-provider.ts @@ -1,7 +1,10 @@ -import type { Owner } from 'solid-js'; import { z } from 'zod'; -import { createProviderListener } from '../create-provider-listener'; +import { + createBaseProvider, + type Provider, +} from '../create-base-provider'; +import { onProviderEmit } from '~/desktop'; export interface IpProviderConfig { type: 'ip'; @@ -12,12 +15,14 @@ export interface IpProviderConfig { refreshInterval?: number; } -const IpProviderConfigSchema = z.object({ +const ipProviderConfigSchema = z.object({ type: z.literal('ip'), refreshInterval: z.coerce.number().default(60 * 60 * 1000), }); -export interface IpProvider { +export type IpProvider = Provider; + +export interface IpOutput { address: string; approxCity: string; approxCountry: string; @@ -27,30 +32,16 @@ export interface IpProvider { export async function createIpProvider( config: IpProviderConfig, - owner: Owner, -) { - const mergedConfig = IpProviderConfigSchema.parse(config); - - const ipVariables = await createProviderListener< - IpProviderConfig, - IpProvider - >(mergedConfig, owner); - - return { - get address() { - return ipVariables().address; - }, - get approxCity() { - return ipVariables().approxCity; - }, - get approxCountry() { - return ipVariables().approxCountry; - }, - get approxLatitude() { - return ipVariables().approxLatitude; - }, - get approxLongitude() { - return ipVariables().approxLongitude; - }, - }; +): Promise { + const mergedConfig = ipProviderConfigSchema.parse(config); + + return createBaseProvider(mergedConfig, async queue => { + return onProviderEmit(mergedConfig, ({ variables }) => { + if ('error' in variables) { + queue.error(variables.error); + } else { + queue.value(variables.data); + } + }); + }); } diff --git a/packages/client-api/src/providers/komorebi/create-komorebi-provider.ts b/packages/client-api/src/providers/komorebi/create-komorebi-provider.ts index fb81a883..cca7461a 100644 --- a/packages/client-api/src/providers/komorebi/create-komorebi-provider.ts +++ b/packages/client-api/src/providers/komorebi/create-komorebi-provider.ts @@ -1,20 +1,26 @@ -import { createEffect, runWithOwner, type Owner } from 'solid-js'; -import { createStore } from 'solid-js/store'; import { z } from 'zod'; -import { createProviderListener } from '../create-provider-listener'; -import { getMonitors } from '~/desktop'; +import { getMonitors, onProviderEmit } from '~/desktop'; import { getCoordinateDistance } from '~/utils'; +import { + createBaseProvider, + type Provider, +} from '../create-base-provider'; export interface KomorebiProviderConfig { type: 'komorebi'; } -const KomorebiProviderConfigSchema = z.object({ +const komorebiProviderConfigSchema = z.object({ type: z.literal('komorebi'), }); -export interface KomorebiProvider { +export type KomorebiProvider = Provider< + KomorebiProviderConfig, + KomorebiOutput +>; + +export interface KomorebiOutput { /** * Workspace displayed on the current monitor. */ @@ -117,95 +123,71 @@ export type KomorebiLayoutFlip = export async function createKomorebiProvider( config: KomorebiProviderConfig, - owner: Owner, ): Promise { - const mergedConfig = KomorebiProviderConfigSchema.parse(config); - const monitors = await getMonitors(); - - const providerListener = await createProviderListener< - KomorebiProviderConfig, - KomorebiResponse - >(mergedConfig, owner); - - const [komorebiVariables, setKomorebiVariables] = createStore( - await getVariables(), - ); - - runWithOwner(owner, () => { - createEffect(async () => setKomorebiVariables(await getVariables())); - }); - - async function getVariables() { - const state = providerListener(); - - const currentPosition = { - x: monitors.currentMonitor!.x, - y: monitors.currentMonitor!.y, - }; - - // Get Komorebi monitor that corresponds to the Zebar window's monitor. - const currentKomorebiMonitor = state.allMonitors.reduce((a, b) => - getCoordinateDistance(currentPosition, { - x: a.workAreaSize.left, - y: a.workAreaSize.top, - }) < - getCoordinateDistance(currentPosition, { - x: b.workAreaSize.left, - y: b.workAreaSize.top, - }) - ? a - : b, + const mergedConfig = komorebiProviderConfigSchema.parse(config); + + // TODO: Update state when monitors change. + return createBaseProvider(mergedConfig, async queue => { + const monitors = await getMonitors(); + + async function getUpdatedState(res: KomorebiResponse) { + const currentPosition = { + x: monitors.currentMonitor!.x, + y: monitors.currentMonitor!.y, + }; + + // Get Komorebi monitor that corresponds to the Zebar window's monitor. + const currentKomorebiMonitor = res.allMonitors.reduce((a, b) => + getCoordinateDistance(currentPosition, { + x: a.workAreaSize.left, + y: a.workAreaSize.top, + }) < + getCoordinateDistance(currentPosition, { + x: b.workAreaSize.left, + y: b.workAreaSize.top, + }) + ? a + : b, + ); + + const displayedKomorebiWorkspace = + currentKomorebiMonitor.workspaces[ + currentKomorebiMonitor.focusedWorkspaceIndex + ]!; + + const allKomorebiWorkspaces = res.allMonitors.flatMap( + monitor => monitor.workspaces, + ); + + const focusedKomorebiMonitor = + res.allMonitors[res.focusedMonitorIndex]!; + + const focusedKomorebiWorkspace = + focusedKomorebiMonitor.workspaces[ + focusedKomorebiMonitor.focusedWorkspaceIndex + ]!; + + return { + displayedWorkspace: displayedKomorebiWorkspace, + focusedWorkspace: focusedKomorebiWorkspace, + currentWorkspaces: currentKomorebiMonitor.workspaces, + allWorkspaces: allKomorebiWorkspaces, + focusedMonitor: focusedKomorebiMonitor, + currentMonitor: currentKomorebiMonitor, + allMonitors: res.allMonitors, + }; + } + + return onProviderEmit( + mergedConfig, + async ({ variables }) => { + if ('error' in variables) { + queue.error(variables.error); + } else { + const updatedState = await getUpdatedState(variables.data); + queue.value(updatedState); + } + }, ); - - const displayedKomorebiWorkspace = - currentKomorebiMonitor.workspaces[ - currentKomorebiMonitor.focusedWorkspaceIndex - ]!; - - const allKomorebiWorkspaces = state.allMonitors.flatMap( - monitor => monitor.workspaces, - ); - - const focusedKomorebiMonitor = - state.allMonitors[state.focusedMonitorIndex]!; - - const focusedKomorebiWorkspace = - focusedKomorebiMonitor.workspaces[ - focusedKomorebiMonitor.focusedWorkspaceIndex - ]!; - - return { - displayedWorkspace: displayedKomorebiWorkspace, - focusedWorkspace: focusedKomorebiWorkspace, - currentWorkspaces: currentKomorebiMonitor.workspaces, - allWorkspaces: allKomorebiWorkspaces, - focusedMonitor: focusedKomorebiMonitor, - currentMonitor: currentKomorebiMonitor, - allMonitors: state.allMonitors, - }; - } - - return { - get displayedWorkspace() { - return komorebiVariables.displayedWorkspace; - }, - get focusedWorkspace() { - return komorebiVariables.focusedWorkspace; - }, - get currentWorkspaces() { - return komorebiVariables.currentWorkspaces; - }, - get allWorkspaces() { - return komorebiVariables.allWorkspaces; - }, - get allMonitors() { - return komorebiVariables.allMonitors; - }, - get focusedMonitor() { - return komorebiVariables.focusedMonitor; - }, - get currentMonitor() { - return komorebiVariables.currentMonitor; - }, - }; + }); } diff --git a/packages/client-api/src/providers/memory/create-memory-provider.ts b/packages/client-api/src/providers/memory/create-memory-provider.ts index ea0f667d..c3e1636c 100644 --- a/packages/client-api/src/providers/memory/create-memory-provider.ts +++ b/packages/client-api/src/providers/memory/create-memory-provider.ts @@ -1,7 +1,10 @@ -import type { Owner } from 'solid-js'; import { z } from 'zod'; -import { createProviderListener } from '../create-provider-listener'; +import { + createBaseProvider, + type Provider, +} from '../create-base-provider'; +import { onProviderEmit } from '~/desktop'; export interface MemoryProviderConfig { type: 'memory'; @@ -12,12 +15,14 @@ export interface MemoryProviderConfig { refreshInterval?: number; } -const MemoryProviderConfigSchema = z.object({ +const memoryProviderConfigSchema = z.object({ type: z.literal('memory'), refreshInterval: z.coerce.number().default(5 * 1000), }); -export interface MemoryProvider { +export type MemoryProvider = Provider; + +export interface MemoryOutput { usage: number; freeMemory: number; usedMemory: number; @@ -29,36 +34,16 @@ export interface MemoryProvider { export async function createMemoryProvider( config: MemoryProviderConfig, - owner: Owner, -) { - const mergedConfig = MemoryProviderConfigSchema.parse(config); - - const memoryVariables = await createProviderListener< - MemoryProviderConfig, - MemoryProvider - >(mergedConfig, owner); - - return { - get usage() { - return memoryVariables().usage; - }, - get freeMemory() { - return memoryVariables().freeMemory; - }, - get usedMemory() { - return memoryVariables().usedMemory; - }, - get totalMemory() { - return memoryVariables().totalMemory; - }, - get freeSwap() { - return memoryVariables().freeSwap; - }, - get usedSwap() { - return memoryVariables().usedSwap; - }, - get totalSwap() { - return memoryVariables().totalSwap; - }, - }; +): Promise { + const mergedConfig = memoryProviderConfigSchema.parse(config); + + return createBaseProvider(mergedConfig, async queue => { + return onProviderEmit(mergedConfig, ({ variables }) => { + if ('error' in variables) { + queue.error(variables.error); + } else { + queue.value(variables.data); + } + }); + }); } diff --git a/packages/client-api/src/providers/network/create-network-provider.ts b/packages/client-api/src/providers/network/create-network-provider.ts index f551998e..d4230640 100644 --- a/packages/client-api/src/providers/network/create-network-provider.ts +++ b/packages/client-api/src/providers/network/create-network-provider.ts @@ -1,7 +1,10 @@ -import type { Owner } from 'solid-js'; import { z } from 'zod'; -import { createProviderListener } from '../create-provider-listener'; +import { + createBaseProvider, + type Provider, +} from '../create-base-provider'; +import { onProviderEmit } from '~/desktop'; export interface NetworkProviderConfig { type: 'network'; @@ -12,12 +15,17 @@ export interface NetworkProviderConfig { refreshInterval?: number; } -const NetworkProviderConfigSchema = z.object({ +const networkProviderConfigSchema = z.object({ type: z.literal('network'), refreshInterval: z.coerce.number().default(5 * 1000), }); -export interface NetworkProvider { +export type NetworkProvider = Provider< + NetworkProviderConfig, + NetworkOutput +>; + +export interface NetworkOutput { defaultInterface: NetworkInterface | null; defaultGateway: NetworkGateway | null; interfaces: NetworkInterface[]; @@ -72,27 +80,16 @@ export interface NetworkTraffic { export async function createNetworkProvider( config: NetworkProviderConfig, - owner: Owner, -) { - const mergedConfig = NetworkProviderConfigSchema.parse(config); - - const networkVariables = await createProviderListener< - NetworkProviderConfig, - NetworkProvider - >(mergedConfig, owner); +): Promise { + const mergedConfig = networkProviderConfigSchema.parse(config); - return { - get defaultInterface() { - return networkVariables().defaultInterface; - }, - get defaultGateway() { - return networkVariables().defaultGateway; - }, - get interfaces() { - return networkVariables().interfaces; - }, - get traffic() { - return networkVariables().traffic; - }, - }; + return createBaseProvider(mergedConfig, async queue => { + return onProviderEmit(mergedConfig, ({ variables }) => { + if ('error' in variables) { + queue.error(variables.error); + } else { + queue.value(variables.data); + } + }); + }); } diff --git a/packages/client-api/src/providers/weather/create-weather-provider.ts b/packages/client-api/src/providers/weather/create-weather-provider.ts index af768568..f235e421 100644 --- a/packages/client-api/src/providers/weather/create-weather-provider.ts +++ b/packages/client-api/src/providers/weather/create-weather-provider.ts @@ -1,12 +1,13 @@ -import type { Owner } from 'solid-js'; import { z } from 'zod'; -import { - type IpProvider, - createIpProvider, -} from '../ip/create-ip-provider'; +import type { IpProvider } from '../ip/create-ip-provider'; import { WeatherStatus } from './weather-status.enum'; -import { createProviderListener } from '../create-provider-listener'; +import { + createBaseProvider, + type Provider, +} from '../create-base-provider'; +import { onProviderEmit } from '~/desktop'; +import { createProvider } from '../create-provider'; export interface WeatherProviderConfig { type: 'weather'; @@ -29,14 +30,19 @@ export interface WeatherProviderConfig { refreshInterval?: number; } -const WeatherProviderConfigSchema = z.object({ +const weatherProviderConfigSchema = z.object({ type: z.literal('weather'), latitude: z.coerce.number().optional(), longitude: z.coerce.number().optional(), refreshInterval: z.coerce.number().default(60 * 60 * 1000), }); -export interface WeatherProvider { +export type WeatherProvider = Provider< + WeatherProviderConfig, + WeatherOutput +>; + +export interface WeatherOutput { isDaytime: boolean; status: WeatherStatus; celsiusTemp: number; @@ -46,49 +52,30 @@ export interface WeatherProvider { export async function createWeatherProvider( config: WeatherProviderConfig, - owner: Owner, -) { +): Promise { let ipProvider: IpProvider | null = null; const mergedConfig: WeatherProviderConfig = { - ...WeatherProviderConfigSchema.parse(config), - longitude: config.longitude ?? (await getIpProvider()).approxLongitude, - latitude: config.latitude ?? (await getIpProvider()).approxLatitude, + ...weatherProviderConfigSchema.parse(config), + longitude: + config.longitude ?? (await getIpProvider()).value!.approxLongitude, + latitude: + config.latitude ?? (await getIpProvider()).value!.approxLatitude, }; - const weatherVariables = await createProviderListener< - WeatherProviderConfig, - WeatherProvider - >(mergedConfig, owner); - async function getIpProvider() { return ( - ipProvider ?? - (ipProvider = await createIpProvider( - { - type: 'ip', - refreshInterval: 60 * 60 * 1000, - }, - owner, - )) + ipProvider ?? (ipProvider = await createProvider({ type: 'ip' })) ); } - return { - get isDaytime() { - return weatherVariables().isDaytime; - }, - get status() { - return weatherVariables().status; - }, - get celsiusTemp() { - return weatherVariables().celsiusTemp; - }, - get fahrenheitTemp() { - return weatherVariables().fahrenheitTemp; - }, - get windSpeed() { - return weatherVariables().windSpeed; - }, - }; + return createBaseProvider(mergedConfig, async queue => { + return onProviderEmit(mergedConfig, ({ variables }) => { + if ('error' in variables) { + queue.error(variables.error); + } else { + queue.value(variables.data); + } + }); + }); } From feb8bf5cbb0475bff1259283a78c0f0f92a5491c Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Tue, 3 Sep 2024 17:37:55 +0800 Subject: [PATCH 073/138] feat: implement provider restart; rename `shutdown` -> `destroy` --- .../src/providers/create-base-provider.ts | 94 ++++++++++--------- 1 file changed, 49 insertions(+), 45 deletions(-) diff --git a/packages/client-api/src/providers/create-base-provider.ts b/packages/client-api/src/providers/create-base-provider.ts index a210d4c8..cc41bc09 100644 --- a/packages/client-api/src/providers/create-base-provider.ts +++ b/packages/client-api/src/providers/create-base-provider.ts @@ -3,14 +3,18 @@ import type { ProviderConfig } from './create-provider'; export interface Provider { /** - * Current value of the provider. + * Latest value emitted from the provider. + * + * `null` if the provider currently has an error. */ - value?: TVal; + value: TVal | null; /** - * Error message from the provider. + * Latest error message emitted from the provider. + * + * `null` if the provider currently has a valid value. */ - error?: string; + error: string | null; /** * Whether the provider currently has an error. @@ -23,14 +27,14 @@ export interface Provider { config: TConfig; /** - * Restart the provider. + * Restarts the provider. */ - restart(): Promise; + restart(): Promise; /** * Stops the provider. */ - shutdown(): Promise; + destroy(): Promise; /** * Listens for changes to the provider's value. @@ -47,6 +51,9 @@ export interface Provider { type UnlistenFn = () => void | Promise; +/** + * Fetches next value or error from the provider. + */ type ProviderFetcher = (queue: { value: (nextVal: T) => void; error: (nextError: string) => void; @@ -59,10 +66,8 @@ export async function createBaseProvider< config: TConfig, fetcher: ProviderFetcher, ): Promise> { - const hasFirstEmit = new Deferred(); - - const valueListeners: ((val: TVal) => void)[] = []; - const errorListeners: ((error: string) => void)[] = []; + const valueListeners = new Set<(val: TVal) => void>(); + const errorListeners = new Set<(error: string) => void>(); let latestEmission = { value: null as TVal | null, @@ -70,40 +75,34 @@ export async function createBaseProvider< hasError: false, }; - const shutdown = fetcher({ - value: value => { - hasFirstEmit.resolve(true); - - latestEmission = { - value, - error: null, - hasError: false, - }; + let unlisten: UnlistenFn | null = await startFetcher(); - valueListeners.forEach(listener => listener(value)); - }, - error: error => { - hasFirstEmit.resolve(true); + async function startFetcher() { + const hasFirstEmit = new Deferred(); - latestEmission = { - value: null, - error, - hasError: false, - }; + const unlisten = await fetcher({ + value: value => { + latestEmission = { value, error: null, hasError: false }; + valueListeners.forEach(listener => listener(value)); + hasFirstEmit.resolve(); + }, + error: error => { + latestEmission = { value: null, error, hasError: true }; + errorListeners.forEach(listener => listener(error)); + hasFirstEmit.resolve(); + }, + }); - errorListeners.forEach(listener => listener(error)); - }, - }); + // Wait for the first emission. + await hasFirstEmit.promise; - // Wait for the first emit before returning the provider. - await hasFirstEmit.promise; + return unlisten; + } return { - // @ts-ignore - TODO get value() { return latestEmission.value; }, - // @ts-ignore - TODO get error() { return latestEmission.error; }, @@ -112,20 +111,25 @@ export async function createBaseProvider< }, config, restart: async () => { - // TODO: Implement restart. - return null as any; + if (unlisten) { + await unlisten(); + } + + await startFetcher(); }, - shutdown: async () => { - valueListeners.length = 0; - errorListeners.length = 0; - const shutdownFn = await shutdown; - await shutdownFn(); + destroy: async () => { + valueListeners.clear(); + errorListeners.clear(); + + if (unlisten) { + await unlisten(); + } }, onValue: callback => { - valueListeners.push(callback); + valueListeners.add(callback); }, onError: callback => { - errorListeners.push(callback); + errorListeners.add(callback); }, }; } From de94b6523b3f4b76905d8a652e878e7bbe7df59f Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Tue, 3 Sep 2024 17:55:09 +0800 Subject: [PATCH 074/138] feat: change window configs to require `.zebar.json` extension --- packages/desktop/resources/config/settings.json | 2 +- .../starter/{glazewm.json => glazewm.zebar.json} | 0 .../starter/{komorebi.json => komorebi.zebar.json} | 0 packages/desktop/src/common/fs_util.rs | 10 +++++++--- packages/desktop/src/config.rs | 9 ++++----- 5 files changed, 12 insertions(+), 9 deletions(-) rename packages/desktop/resources/config/starter/{glazewm.json => glazewm.zebar.json} (100%) rename packages/desktop/resources/config/starter/{komorebi.json => komorebi.zebar.json} (100%) diff --git a/packages/desktop/resources/config/settings.json b/packages/desktop/resources/config/settings.json index ed5945ed..10de35f3 100644 --- a/packages/desktop/resources/config/settings.json +++ b/packages/desktop/resources/config/settings.json @@ -1,4 +1,4 @@ { "$schema": "TODO", - "startupConfigs": ["starter/glazewm.json"] + "startupConfigs": ["starter/glazewm.zebar.json"] } diff --git a/packages/desktop/resources/config/starter/glazewm.json b/packages/desktop/resources/config/starter/glazewm.zebar.json similarity index 100% rename from packages/desktop/resources/config/starter/glazewm.json rename to packages/desktop/resources/config/starter/glazewm.zebar.json diff --git a/packages/desktop/resources/config/starter/komorebi.json b/packages/desktop/resources/config/starter/komorebi.zebar.json similarity index 100% rename from packages/desktop/resources/config/starter/komorebi.json rename to packages/desktop/resources/config/starter/komorebi.zebar.json diff --git a/packages/desktop/src/common/fs_util.rs b/packages/desktop/src/common/fs_util.rs index a43d8cc5..9ff77e3a 100644 --- a/packages/desktop/src/common/fs_util.rs +++ b/packages/desktop/src/common/fs_util.rs @@ -19,9 +19,13 @@ pub fn read_and_parse_json( Ok(parsed) } -/// Returns whether the given path is a JSON file. -pub fn is_json(path: &PathBuf) -> bool { - path.extension().and_then(|ext| ext.to_str()) == Some("json") +/// Returns whether the path has the given extension. +pub fn has_extension(path: &PathBuf, extension: &str) -> bool { + path + .file_name() + .and_then(|name| name.to_str()) + .map(|name| name.ends_with(extension)) + .unwrap_or(false) } /// Recursively copies a directory and all its contents to a new file diff --git a/packages/desktop/src/config.rs b/packages/desktop/src/config.rs index 524b358b..31f0ef5f 100644 --- a/packages/desktop/src/config.rs +++ b/packages/desktop/src/config.rs @@ -8,10 +8,10 @@ use anyhow::Context; use serde::{Deserialize, Serialize}; use tauri::{path::BaseDirectory, AppHandle, Manager}; use tokio::sync::{broadcast, Mutex}; -use tracing::{info, warn}; +use tracing::{error, info}; use crate::common::{ - copy_dir_all, is_json, read_and_parse_json, LengthValue, PathExt, + copy_dir_all, has_extension, read_and_parse_json, LengthValue, PathExt, }; #[derive(Clone, Debug, Deserialize, Serialize)] @@ -247,7 +247,7 @@ impl Config { if path.is_dir() { // Recursively aggregate configs in subdirectories. configs.extend(Self::read_window_configs(&path)?); - } else if is_json(&path) { + } else if has_extension(&path, ".zebar.json") { if let Ok(config) = read_and_parse_json::(&path) { let config_path = path.canonicalize_pretty()?; @@ -265,8 +265,7 @@ impl Config { html_path, }); } else { - // TODO: Show error dialog. - warn!("Failed to process file: {}", path.display()); + error!("Failed to parse config: {}", path.display()); } } } From 919bf473eaaa19d3e648bda24cf33a0b5b482768 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Wed, 4 Sep 2024 17:06:22 +0800 Subject: [PATCH 075/138] refactor: separate out setup for single instance --- packages/desktop/src/cli.rs | 8 +- packages/desktop/src/main.rs | 146 +++++++++++++++++++---------------- 2 files changed, 82 insertions(+), 72 deletions(-) diff --git a/packages/desktop/src/cli.rs b/packages/desktop/src/cli.rs index 22bc77ae..a932dbbe 100644 --- a/packages/desktop/src/cli.rs +++ b/packages/desktop/src/cli.rs @@ -17,7 +17,7 @@ impl Cli { } } -#[derive(Clone, Debug, Subcommand)] +#[derive(Clone, Debug, PartialEq, Subcommand)] pub enum CliCommand { /// Open a window by its config path. /// @@ -43,7 +43,7 @@ pub enum CliCommand { Empty, } -#[derive(Args, Clone, Debug)] +#[derive(Args, Clone, Debug, PartialEq)] pub struct OpenWindowArgs { /// Relative file path to window config within the Zebar config /// directory. @@ -56,7 +56,7 @@ pub struct OpenWindowArgs { pub config_dir: Option, } -#[derive(Args, Clone, Debug)] +#[derive(Args, Clone, Debug, PartialEq)] pub struct OpenAllWindowsArgs { /// Absolute path to the Zebar config directory. /// @@ -65,7 +65,7 @@ pub struct OpenAllWindowsArgs { pub config_dir: Option, } -#[derive(Args, Clone, Debug)] +#[derive(Args, Clone, Debug, PartialEq)] pub struct OutputMonitorsArgs { /// Use ASCII NUL character (character code 0) instead of newlines /// for delimiting monitors. diff --git a/packages/desktop/src/main.rs b/packages/desktop/src/main.rs index 56e7db78..3872af66 100644 --- a/packages/desktop/src/main.rs +++ b/packages/desktop/src/main.rs @@ -100,49 +100,13 @@ fn start_app(cli: Cli) -> anyhow::Result<()> { app.manage(window_factory.clone()); // If this is not the first instance of the app, this will emit - // within the original instance and exit immediately. - let config_clone = config.clone(); - let window_factory_clone = window_factory.clone(); - app.handle().plugin(tauri_plugin_single_instance::init( - move |_, args, _| { - let cli = Cli::parse_from(args); - - // TODO: Improve this. - let config_clone = config_clone.clone(); - let window_factory_clone = window_factory_clone.clone(); - - // CLI command is guaranteed to be one of the open commands - // here. - task::spawn(async move { - if let CliCommand::Open(args) = cli.command() { - let window_config_res = config_clone - .window_config_by_path( - &config_clone.join_path(&args.config_path), - ) - .await - .and_then(|res| { - res.ok_or_else(|| { - anyhow::anyhow!( - "Window config not found at {}.", - args.config_path - ) - }) - }); - - match window_config_res { - Ok(window_config) => { - let window_factory_clone = - window_factory_clone.clone(); - window_factory_clone.open(window_config).await; - } - Err(err) => { - error!("{:?}", err); - } - } - } - }); - }, - ))?; + // within the original instance and exit immediately. The CLI + // command is guaranteed to be one of the open commands here. + setup_single_instance( + app, + config.clone(), + window_factory.clone(), + )?; // Prevent windows from showing up in the dock on MacOS. #[cfg(target_os = "macos")] @@ -153,31 +117,13 @@ fn start_app(cli: Cli) -> anyhow::Result<()> { .asset_protocol_scope() .allow_directory(&config.config_dir, true)?; - // Get window configs to open on start. - let window_configs = match cli.command() { - CliCommand::Open(args) => { - let window_config = config - .window_config_by_path( - &config.join_path(&args.config_path), - ) - .await? - .with_context(|| { - format!( - "Window config not found at {}.", - args.config_path - ) - })?; - - vec![window_config] - } - _ => config.startup_window_configs().await?, - }; - - for window_config in window_configs { - if let Err(err) = window_factory.open(window_config).await { - error!("Failed to open window: {:?}", err); - } - } + // Open windows based on CLI command. + open_windows_by_cli_command( + cli, + config.clone(), + window_factory.clone(), + ) + .await?; app.handle().plugin(tauri_plugin_shell::init())?; app.handle().plugin(tauri_plugin_http::init())?; @@ -215,3 +161,67 @@ fn start_app(cli: Cli) -> anyhow::Result<()> { Ok(()) } + +/// Setup single instance Tauri plugin. +fn setup_single_instance( + app: &tauri::App, + config: Arc, + window_factory: Arc, +) -> anyhow::Result<()> { + app.handle().plugin(tauri_plugin_single_instance::init( + move |_, args, _| { + let config = config.clone(); + let window_factory = window_factory.clone(); + + task::spawn(async move { + let res = match Cli::try_parse_from(args) { + Ok(cli) => { + // No-op if no subcommand is provided. + if cli.command() != CliCommand::Empty { + open_windows_by_cli_command(cli, config, window_factory) + .await + } else { + Ok(()) + } + } + _ => Err(anyhow::anyhow!("Failed to parse CLI arguments.")), + }; + + if let Err(err) = res { + error!("{:?}", err); + } + }); + }, + ))?; + + Ok(()) +} + +/// Opens windows based on CLI command. +async fn open_windows_by_cli_command( + cli: Cli, + config: Arc, + window_factory: Arc, +) -> anyhow::Result<()> { + let window_configs = match cli.command() { + CliCommand::Open(args) => { + let window_config = config + .window_config_by_path(&config.join_path(&args.config_path)) + .await? + .with_context(|| { + format!("Window config not found at {}.", args.config_path) + })?; + + vec![window_config] + } + _ => config.startup_window_configs().await?, + }; + + for window_config in window_configs { + if let Err(err) = window_factory.open(window_config).await { + error!("Failed to open window: {:?}", err); + } + } + + Ok(()) +} From a2e402e3782780905df2fb594764aefe689930c6 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 5 Sep 2024 14:02:35 +0800 Subject: [PATCH 076/138] feat: remove solid-js dep in client-api --- package.json | 2 +- packages/client-api/package.json | 17 +++----- packages/client-api/src/desktop/monitors.ts | 8 ++-- packages/client-api/tsup.config.ts | 47 --------------------- pnpm-lock.yaml | 45 -------------------- 5 files changed, 12 insertions(+), 107 deletions(-) delete mode 100644 packages/client-api/tsup.config.ts diff --git a/package.json b/package.json index 30cee86e..cf214979 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ ], "scripts": { "build": "pnpm run -r build", - "dev": "pnpm run --parallel dev", + "dev": "pnpm run --filter zebar build && pnpm run --parallel dev", "format": "prettier --write . && pnpm run -r format", "lint": "prettier --check . && pnpm run -r lint" }, diff --git a/packages/client-api/package.json b/packages/client-api/package.json index ddf17c8e..5629e18c 100644 --- a/packages/client-api/package.json +++ b/packages/client-api/package.json @@ -1,20 +1,19 @@ { "name": "zebar", "version": "2.0.0", - "type": "module", + "sideEffects": false, "exports": { - "import": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" + ".": { + "require": "./dist/index.js", + "import": "./dist/index.mjs", + "types": "./dist/index.d.ts" } }, "main": "./dist/index.js", - "module": "./dist/index.js", - "browser": {}, + "module": "./dist/index.mjs", "types": "./dist/index.d.ts", - "typesVersions": {}, "scripts": { - "build": "tsup", + "build": "tsup src/index.ts --format cjs,esm --dts --inject-style", "dev": "npm run build -- --watch src", "prepublishOnly": "npm run build" }, @@ -24,14 +23,12 @@ "@tauri-apps/plugin-shell": "2.0.0-beta.8", "glazewm": "1.4.1", "luxon": "3.4.4", - "solid-js": "1.8.14", "zod": "3.22.4" }, "devDependencies": { "@types/luxon": "3.4.2", "esbuild": "0.20.0", "tsup": "8.0.2", - "tsup-preset-solid": "2.2.0", "typescript": "5.3.3" } } diff --git a/packages/client-api/src/desktop/monitors.ts b/packages/client-api/src/desktop/monitors.ts index e6142fab..db74f40b 100644 --- a/packages/client-api/src/desktop/monitors.ts +++ b/packages/client-api/src/desktop/monitors.ts @@ -5,7 +5,6 @@ import { primaryMonitor as getPrimaryMonitor, getCurrentWindow, } from '@tauri-apps/api/window'; -import { createStore } from 'solid-js/store'; import type { MonitorInfo } from './shared'; @@ -37,12 +36,12 @@ async function createMonitorCache() { // return value, and refresh it in an effect when displays are changed. // Ref https://github.com/tauri-apps/tauri/issues/8405 - const [monitorCache, setMonitorCache] = createStore({ + const monitorCache = { currentMonitor: currentMonitor ? toMonitorInfo(currentMonitor) : null, primaryMonitor: primaryMonitor ? toMonitorInfo(primaryMonitor) : null, secondaryMonitors: secondaryMonitors.map(toMonitorInfo), allMonitors: allMonitors.map(toMonitorInfo), - }); + }; getCurrentWindow().onResized(() => updateCurrentMonitor()); getCurrentWindow().onMoved(() => updateCurrentMonitor()); @@ -51,7 +50,8 @@ async function createMonitorCache() { async function updateCurrentMonitor() { const currentMonitor = await getCurrentMonitor(); - setMonitorCache({ + // TODO: Avoid mutating the cache object. + Object.assign(monitorCache, { currentMonitor: currentMonitor ? toMonitorInfo(currentMonitor) : null, diff --git a/packages/client-api/tsup.config.ts b/packages/client-api/tsup.config.ts deleted file mode 100644 index f969fc41..00000000 --- a/packages/client-api/tsup.config.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { defineConfig } from 'tsup'; -import * as preset from 'tsup-preset-solid'; - -const preset_options: preset.PresetOptions = { - // array or single object - entries: [ - // default entry (index) - { - // entries with '.tsx' extension will have `solid` export condition generated - entry: 'src/index.ts', - }, - ], - // Set to `true` to remove all `console.*` calls and `debugger` statements in prod builds - drop_console: false, - // Set to `true` to generate a CommonJS build alongside ESM - // cjs: true, -}; - -const CI = - process.env['CI'] === 'true' || - process.env['GITHUB_ACTIONS'] === 'true' || - process.env['CI'] === '"1"' || - process.env['GITHUB_ACTIONS'] === '"1"'; - -export default defineConfig(config => { - const watching = !!config.watch; - - config.injectStyle = true; - - const parsed_options = preset.parsePresetOptions( - preset_options, - watching, - ); - - if (!watching && !CI) { - const package_fields = preset.generatePackageExports(parsed_options); - - console.log( - `package.json: \n\n${JSON.stringify(package_fields, null, 2)}\n\n`, - ); - - // will update ./package.json with the correct export fields - preset.writePackageJson(package_fields); - } - - return preset.generateTsupOptions(parsed_options); -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3c9a7daf..4ad4071c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,9 +51,6 @@ importers: luxon: specifier: 3.4.4 version: 3.4.4 - solid-js: - specifier: 1.8.14 - version: 1.8.14 zod: specifier: 3.22.4 version: 3.22.4 @@ -67,9 +64,6 @@ importers: tsup: specifier: 8.0.2 version: 8.0.2(postcss@8.4.35)(typescript@5.3.3) - tsup-preset-solid: - specifier: 2.2.0 - version: 2.2.0(esbuild@0.20.0)(solid-js@1.8.14)(tsup@8.0.2(postcss@8.4.35)(typescript@5.3.3)) typescript: specifier: 5.3.3 version: 5.3.3 @@ -901,12 +895,6 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - esbuild-plugin-solid@0.5.0: - resolution: {integrity: sha512-ITK6n+0ayGFeDVUZWNMxX+vLsasEN1ILrg4pISsNOQ+mq4ljlJJiuXotInd+HE0MzwTcA9wExT1yzDE2hsqPsg==} - peerDependencies: - esbuild: '>=0.12' - solid-js: '>= 1.0' - esbuild@0.19.11: resolution: {integrity: sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==} engines: {node: '>=12'} @@ -1339,9 +1327,6 @@ packages: solid-js@1.8.11: resolution: {integrity: sha512-WdwmER+TwBJiN4rVQTVBxocg+9pKlOs41KzPYntrC86xO5sek8TzBYozPEZPL1IRWDouf2lMrvSbIs3CanlPvQ==} - solid-js@1.8.14: - resolution: {integrity: sha512-kDfgHBm+ROVLDVuqaXh/jYz0ZVJ29TYfVsKsgDPtNcjoyaPtOvDX2l0tVnthjLdEXr7vDTYeqEYFfMkZakDsOQ==} - solid-refresh@0.6.3: resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==} peerDependencies: @@ -1437,11 +1422,6 @@ packages: tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - tsup-preset-solid@2.2.0: - resolution: {integrity: sha512-sPAzeArmYkVAZNRN+m4tkiojdd0GzW/lCwd4+TQDKMENe8wr2uAuro1s0Z59ASmdBbkXoxLgCiNcuQMyiidMZg==} - peerDependencies: - tsup: ^8.0.0 - tsup@8.0.2: resolution: {integrity: sha512-NY8xtQXdH7hDUAZwcQdY/Vzlw9johQsaqf7iwZ6g1DOUlFYQ5/AtVAjTvihhEyeRlGo4dLRVHtrRaL35M1daqQ==} engines: {node: '>=18'} @@ -2232,16 +2212,6 @@ snapshots: emoji-regex@9.2.2: {} - esbuild-plugin-solid@0.5.0(esbuild@0.20.0)(solid-js@1.8.14): - dependencies: - '@babel/core': 7.23.7 - '@babel/preset-typescript': 7.23.3(@babel/core@7.23.7) - babel-preset-solid: 1.8.9(@babel/core@7.23.7) - esbuild: 0.20.0 - solid-js: 1.8.14 - transitivePeerDependencies: - - supports-color - esbuild@0.19.11: optionalDependencies: '@esbuild/aix-ppc64': 0.19.11 @@ -2655,12 +2625,6 @@ snapshots: seroval: 1.0.4 seroval-plugins: 1.0.4(seroval@1.0.4) - solid-js@1.8.14: - dependencies: - csstype: 3.1.3 - seroval: 1.0.4 - seroval-plugins: 1.0.4(seroval@1.0.4) - solid-refresh@0.6.3(solid-js@1.8.11): dependencies: '@babel/generator': 7.23.6 @@ -2761,15 +2725,6 @@ snapshots: tslib@2.6.2: {} - tsup-preset-solid@2.2.0(esbuild@0.20.0)(solid-js@1.8.14)(tsup@8.0.2(postcss@8.4.35)(typescript@5.3.3)): - dependencies: - esbuild-plugin-solid: 0.5.0(esbuild@0.20.0)(solid-js@1.8.14) - tsup: 8.0.2(postcss@8.4.35)(typescript@5.3.3) - transitivePeerDependencies: - - esbuild - - solid-js - - supports-color - tsup@8.0.2(postcss@8.4.35)(typescript@5.3.3): dependencies: bundle-require: 4.0.2(esbuild@0.19.11) From e6259c24692e9c57247fa2cf4b60bccbe17f46be Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 5 Sep 2024 15:03:54 +0800 Subject: [PATCH 077/138] feat: rename `value`/`onValue` -> `output`/`onOutput` --- examples/solidjs-ts/src/index.tsx | 20 ++++++------ .../battery/create-battery-provider.ts | 2 +- .../src/providers/cpu/create-cpu-provider.ts | 2 +- .../src/providers/create-base-provider.ts | 32 ++++++++++--------- .../src/providers/create-provider.ts | 12 +++++++ .../providers/date/create-date-provider.ts | 4 +-- .../glazewm/create-glazewm-provider.ts | 4 +-- .../providers/host/create-host-provider.ts | 2 +- .../src/providers/ip/create-ip-provider.ts | 2 +- .../komorebi/create-komorebi-provider.ts | 2 +- .../memory/create-memory-provider.ts | 2 +- .../network/create-network-provider.ts | 2 +- .../weather/create-weather-provider.ts | 6 ++-- 13 files changed, 53 insertions(+), 39 deletions(-) diff --git a/examples/solidjs-ts/src/index.tsx b/examples/solidjs-ts/src/index.tsx index d7a8e6f8..2d0a001e 100644 --- a/examples/solidjs-ts/src/index.tsx +++ b/examples/solidjs-ts/src/index.tsx @@ -15,18 +15,18 @@ render(() => , document.getElementById('root')!); function App() { const [store, setStore] = createStore({ - glazewm: glazewm.value, - cpu: cpu.value, - battery: battery.value, - memory: memory.value, - weather: weather.value, + glazewm: glazewm.output, + cpu: cpu.output, + battery: battery.output, + memory: memory.output, + weather: weather.output, }); - glazewm.onValue(glazewm => setStore({ glazewm })); - cpu.onValue(cpu => setStore({ cpu })); - battery.onValue(battery => setStore({ battery })); - memory.onValue(memory => setStore({ memory })); - weather.onValue(weather => setStore({ weather })); + glazewm.onOutput(glazewm => setStore({ glazewm })); + cpu.onOutput(cpu => setStore({ cpu })); + battery.onOutput(battery => setStore({ battery })); + memory.onOutput(memory => setStore({ memory })); + weather.onOutput(weather => setStore({ weather })); return (
diff --git a/packages/client-api/src/providers/battery/create-battery-provider.ts b/packages/client-api/src/providers/battery/create-battery-provider.ts index 014772b6..77bbe914 100644 --- a/packages/client-api/src/providers/battery/create-battery-provider.ts +++ b/packages/client-api/src/providers/battery/create-battery-provider.ts @@ -47,7 +47,7 @@ export async function createBatteryProvider( if ('error' in variables) { queue.error(variables.error); } else { - queue.value(variables.data); + queue.output(variables.data); } }); }); diff --git a/packages/client-api/src/providers/cpu/create-cpu-provider.ts b/packages/client-api/src/providers/cpu/create-cpu-provider.ts index 5dbd3c3b..5a604ea6 100644 --- a/packages/client-api/src/providers/cpu/create-cpu-provider.ts +++ b/packages/client-api/src/providers/cpu/create-cpu-provider.ts @@ -40,7 +40,7 @@ export async function createCpuProvider( if ('error' in variables) { queue.error(variables.error); } else { - queue.value(variables.data); + queue.output(variables.data); } }); }); diff --git a/packages/client-api/src/providers/create-base-provider.ts b/packages/client-api/src/providers/create-base-provider.ts index cc41bc09..bdebb665 100644 --- a/packages/client-api/src/providers/create-base-provider.ts +++ b/packages/client-api/src/providers/create-base-provider.ts @@ -3,26 +3,26 @@ import type { ProviderConfig } from './create-provider'; export interface Provider { /** - * Latest value emitted from the provider. + * Latest output emitted from the provider. * - * `null` if the provider currently has an error. + * `null` if the latest emission from the provider is an error. */ - value: TVal | null; + output: TVal | null; /** * Latest error message emitted from the provider. * - * `null` if the provider currently has a valid value. + * `null` if the latest emission from the provider is a valid output. */ error: string | null; /** - * Whether the provider currently has an error. + * Whether the latest emission from the provider is an error. */ hasError: boolean; /** - * Config for the provider. + * Underlying config for the provider. */ config: TConfig; @@ -34,16 +34,18 @@ export interface Provider { /** * Stops the provider. */ - destroy(): Promise; + stop(): Promise; /** * Listens for changes to the provider's value. - * @param callback - Callback to run when the value changes. + * + * @param callback - Callback to run when an output is emitted. */ - onValue(callback: (nextVal: TVal) => void): void; + onOutput(callback: (output: TVal) => void): void; /** * Listens for errors from the provider. + * * @param callback - Callback to run when an error is emitted. */ onError(callback: (error: string) => void): void; @@ -52,10 +54,10 @@ export interface Provider { type UnlistenFn = () => void | Promise; /** - * Fetches next value or error from the provider. + * Fetches next output or error from the provider. */ type ProviderFetcher = (queue: { - value: (nextVal: T) => void; + output: (nextOutput: T) => void; error: (nextError: string) => void; }) => UnlistenFn | Promise; @@ -81,7 +83,7 @@ export async function createBaseProvider< const hasFirstEmit = new Deferred(); const unlisten = await fetcher({ - value: value => { + output: value => { latestEmission = { value, error: null, hasError: false }; valueListeners.forEach(listener => listener(value)); hasFirstEmit.resolve(); @@ -100,7 +102,7 @@ export async function createBaseProvider< } return { - get value() { + get output() { return latestEmission.value; }, get error() { @@ -117,7 +119,7 @@ export async function createBaseProvider< await startFetcher(); }, - destroy: async () => { + stop: async () => { valueListeners.clear(); errorListeners.clear(); @@ -125,7 +127,7 @@ export async function createBaseProvider< await unlisten(); } }, - onValue: callback => { + onOutput: callback => { valueListeners.add(callback); }, onError: callback => { diff --git a/packages/client-api/src/providers/create-provider.ts b/packages/client-api/src/providers/create-provider.ts index 153baaed..d545b434 100644 --- a/packages/client-api/src/providers/create-provider.ts +++ b/packages/client-api/src/providers/create-provider.ts @@ -77,6 +77,18 @@ export type ProviderOutput = ReturnType< ProviderMap[T] >; +/** + * Creates an instance of a provider. + * + * The provider will continue to output until its `destroy` function is + * called. + * + * Waits until the provider has emitted either its first output or first + * error. + * + * @throws If the provider config is invalid. *Does not throw* if the + * provider initially emits an error. + */ export function createProvider( config: T, ): ProviderOutput { diff --git a/packages/client-api/src/providers/date/create-date-provider.ts b/packages/client-api/src/providers/date/create-date-provider.ts index eb9c0163..668153b4 100644 --- a/packages/client-api/src/providers/date/create-date-provider.ts +++ b/packages/client-api/src/providers/date/create-date-provider.ts @@ -81,10 +81,10 @@ export async function createDateProvider( const mergedConfig = dateProviderConfigSchema.parse(config); return createBaseProvider(mergedConfig, queue => { - queue.value(getDateValue()); + queue.output(getDateValue()); const interval = setInterval( - () => queue.value(getDateValue()), + () => queue.output(getDateValue()), mergedConfig.refreshInterval, ); diff --git a/packages/client-api/src/providers/glazewm/create-glazewm-provider.ts b/packages/client-api/src/providers/glazewm/create-glazewm-provider.ts index 3f3d5a34..2592139d 100644 --- a/packages/client-api/src/providers/glazewm/create-glazewm-provider.ts +++ b/packages/client-api/src/providers/glazewm/create-glazewm-provider.ts @@ -108,7 +108,7 @@ export async function createGlazeWmProvider( const client = new WmClient(); let state = await getInitialState(); - queue.value(state); + queue.output(state); const unlisten = await client.subscribeMany( [ @@ -169,7 +169,7 @@ export async function createGlazeWmProvider( } } - queue.value(state); + queue.output(state); } function focusWorkspace(name: string) { diff --git a/packages/client-api/src/providers/host/create-host-provider.ts b/packages/client-api/src/providers/host/create-host-provider.ts index 5bbd0550..3573f993 100644 --- a/packages/client-api/src/providers/host/create-host-provider.ts +++ b/packages/client-api/src/providers/host/create-host-provider.ts @@ -41,7 +41,7 @@ export async function createHostProvider( if ('error' in variables) { queue.error(variables.error); } else { - queue.value(variables.data); + queue.output(variables.data); } }); }); diff --git a/packages/client-api/src/providers/ip/create-ip-provider.ts b/packages/client-api/src/providers/ip/create-ip-provider.ts index 888a9b21..efeae76d 100644 --- a/packages/client-api/src/providers/ip/create-ip-provider.ts +++ b/packages/client-api/src/providers/ip/create-ip-provider.ts @@ -40,7 +40,7 @@ export async function createIpProvider( if ('error' in variables) { queue.error(variables.error); } else { - queue.value(variables.data); + queue.output(variables.data); } }); }); diff --git a/packages/client-api/src/providers/komorebi/create-komorebi-provider.ts b/packages/client-api/src/providers/komorebi/create-komorebi-provider.ts index cca7461a..de2029a2 100644 --- a/packages/client-api/src/providers/komorebi/create-komorebi-provider.ts +++ b/packages/client-api/src/providers/komorebi/create-komorebi-provider.ts @@ -185,7 +185,7 @@ export async function createKomorebiProvider( queue.error(variables.error); } else { const updatedState = await getUpdatedState(variables.data); - queue.value(updatedState); + queue.output(updatedState); } }, ); diff --git a/packages/client-api/src/providers/memory/create-memory-provider.ts b/packages/client-api/src/providers/memory/create-memory-provider.ts index c3e1636c..4ddf3dfd 100644 --- a/packages/client-api/src/providers/memory/create-memory-provider.ts +++ b/packages/client-api/src/providers/memory/create-memory-provider.ts @@ -42,7 +42,7 @@ export async function createMemoryProvider( if ('error' in variables) { queue.error(variables.error); } else { - queue.value(variables.data); + queue.output(variables.data); } }); }); diff --git a/packages/client-api/src/providers/network/create-network-provider.ts b/packages/client-api/src/providers/network/create-network-provider.ts index d4230640..16bdae07 100644 --- a/packages/client-api/src/providers/network/create-network-provider.ts +++ b/packages/client-api/src/providers/network/create-network-provider.ts @@ -88,7 +88,7 @@ export async function createNetworkProvider( if ('error' in variables) { queue.error(variables.error); } else { - queue.value(variables.data); + queue.output(variables.data); } }); }); diff --git a/packages/client-api/src/providers/weather/create-weather-provider.ts b/packages/client-api/src/providers/weather/create-weather-provider.ts index f235e421..afc558a7 100644 --- a/packages/client-api/src/providers/weather/create-weather-provider.ts +++ b/packages/client-api/src/providers/weather/create-weather-provider.ts @@ -58,9 +58,9 @@ export async function createWeatherProvider( const mergedConfig: WeatherProviderConfig = { ...weatherProviderConfigSchema.parse(config), longitude: - config.longitude ?? (await getIpProvider()).value!.approxLongitude, + config.longitude ?? (await getIpProvider()).output!.approxLongitude, latitude: - config.latitude ?? (await getIpProvider()).value!.approxLatitude, + config.latitude ?? (await getIpProvider()).output!.approxLatitude, }; async function getIpProvider() { @@ -74,7 +74,7 @@ export async function createWeatherProvider( if ('error' in variables) { queue.error(variables.error); } else { - queue.value(variables.data); + queue.output(variables.data); } }); }); From 589c986667aea0e3b246b40890f110322687dd2d Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 5 Sep 2024 15:04:15 +0800 Subject: [PATCH 078/138] feat: format config path without `.zebar.json` suffix --- packages/desktop/src/config.rs | 4 +++- packages/desktop/src/sys_tray.rs | 11 +++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/desktop/src/config.rs b/packages/desktop/src/config.rs index 31f0ef5f..bc50ce64 100644 --- a/packages/desktop/src/config.rs +++ b/packages/desktop/src/config.rs @@ -154,7 +154,9 @@ impl Config { .path() .resolve(".glzr/zebar", BaseDirectory::Home) .context("Unable to get home directory.")?, - }; + } + .canonicalize_pretty()? + .into(); let settings = Self::read_settings_or_init(app_handle, &config_dir)?; let window_configs = Self::read_window_configs(&config_dir)?; diff --git a/packages/desktop/src/sys_tray.rs b/packages/desktop/src/sys_tray.rs index 36552e0d..d95c9d75 100644 --- a/packages/desktop/src/sys_tray.rs +++ b/packages/desktop/src/sys_tray.rs @@ -266,13 +266,12 @@ impl SysTray { config_path: &str, config_dir: &PathBuf, ) -> String { - let config_pathbuf = PathBuf::from(config_path); - - config_pathbuf + let path = PathBuf::from(config_path) .strip_prefix(config_dir) - .unwrap_or(&config_pathbuf) - .with_extension("") + .unwrap_or(&PathBuf::from(config_path)) .to_string_lossy() - .to_string() + .to_string(); + + path.strip_suffix(".zebar.json").unwrap_or(&path).into() } } From 2b5d3012bd91b79aecfc673c45c2cb2d8d31af10 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 5 Sep 2024 15:05:08 +0800 Subject: [PATCH 079/138] feat: remove `glazewm` from solidjs-ts example --- examples/solidjs-ts/src/index.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/examples/solidjs-ts/src/index.tsx b/examples/solidjs-ts/src/index.tsx index 2d0a001e..72c60d20 100644 --- a/examples/solidjs-ts/src/index.tsx +++ b/examples/solidjs-ts/src/index.tsx @@ -5,7 +5,6 @@ import { createStore } from 'solid-js/store'; import { init } from 'zebar'; const zebarCtx = await init(); -const glazewm = await zebarCtx.createProvider({ type: 'glazewm' }); const cpu = await zebarCtx.createProvider({ type: 'cpu' }); const battery = await zebarCtx.createProvider({ type: 'battery' }); const memory = await zebarCtx.createProvider({ type: 'memory' }); @@ -15,14 +14,12 @@ render(() => , document.getElementById('root')!); function App() { const [store, setStore] = createStore({ - glazewm: glazewm.output, cpu: cpu.output, battery: battery.output, memory: memory.output, weather: weather.output, }); - glazewm.onOutput(glazewm => setStore({ glazewm })); cpu.onOutput(cpu => setStore({ cpu })); battery.onOutput(battery => setStore({ battery })); memory.onOutput(memory => setStore({ memory })); @@ -30,7 +27,6 @@ function App() { return (
- glazewm: {JSON.stringify(store.glazewm)} cpu: {store.cpu.usage} battery: {store.battery?.chargePercent} memory: {store.memory.usage} From e3b91fec2c12b85ee3d52ec368ef5d618a862c62 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 5 Sep 2024 19:08:28 +0800 Subject: [PATCH 080/138] refactor: remove `refresh` and `stop` channels; call methods on provider instance directly --- examples/solidjs-ts/src/index.tsx | 11 ++- packages/desktop/src/main.rs | 16 ++-- .../src/providers/komorebi/provider.rs | 5 +- packages/desktop/src/providers/provider.rs | 13 ++- .../desktop/src/providers/provider_manager.rs | 26 +++--- .../desktop/src/providers/provider_ref.rs | 87 +++++-------------- packages/desktop/src/sys_tray.rs | 28 ++++-- 7 files changed, 80 insertions(+), 106 deletions(-) diff --git a/examples/solidjs-ts/src/index.tsx b/examples/solidjs-ts/src/index.tsx index 72c60d20..c19cd189 100644 --- a/examples/solidjs-ts/src/index.tsx +++ b/examples/solidjs-ts/src/index.tsx @@ -3,12 +3,15 @@ import './index.css'; import { render } from 'solid-js/web'; import { createStore } from 'solid-js/store'; import { init } from 'zebar'; +import { createEffect } from 'solid-js'; const zebarCtx = await init(); -const cpu = await zebarCtx.createProvider({ type: 'cpu' }); -const battery = await zebarCtx.createProvider({ type: 'battery' }); -const memory = await zebarCtx.createProvider({ type: 'memory' }); -const weather = await zebarCtx.createProvider({ type: 'weather' }); +const [cpu, battery, memory, weather] = await Promise.all([ + zebarCtx.createProvider({ type: 'cpu' }), + zebarCtx.createProvider({ type: 'battery' }), + zebarCtx.createProvider({ type: 'memory' }), + zebarCtx.createProvider({ type: 'weather' }), +]); render(() => , document.getElementById('root')!); diff --git a/packages/desktop/src/main.rs b/packages/desktop/src/main.rs index 3872af66..e0149a70 100644 --- a/packages/desktop/src/main.rs +++ b/packages/desktop/src/main.rs @@ -117,14 +117,6 @@ fn start_app(cli: Cli) -> anyhow::Result<()> { .asset_protocol_scope() .allow_directory(&config.config_dir, true)?; - // Open windows based on CLI command. - open_windows_by_cli_command( - cli, - config.clone(), - window_factory.clone(), - ) - .await?; - app.handle().plugin(tauri_plugin_shell::init())?; app.handle().plugin(tauri_plugin_http::init())?; app.handle().plugin(tauri_plugin_dialog::init())?; @@ -133,6 +125,14 @@ fn start_app(cli: Cli) -> anyhow::Result<()> { let manager = Arc::new(ProviderManager::new(app.handle())); app.manage(manager); + // Open windows based on CLI command. + open_windows_by_cli_command( + cli, + config.clone(), + window_factory.clone(), + ) + .await?; + // Add application icon to system tray. let _ = SysTray::new(app.handle(), config.clone(), window_factory) diff --git a/packages/desktop/src/providers/komorebi/provider.rs b/packages/desktop/src/providers/komorebi/provider.rs index f387f60c..a13e33f4 100644 --- a/packages/desktop/src/providers/komorebi/provider.rs +++ b/packages/desktop/src/providers/komorebi/provider.rs @@ -178,18 +178,17 @@ impl Provider for KomorebiProvider { }); self.abort_handle = Some(task_handle.abort_handle()); - _ = task_handle.await; } async fn on_refresh( - &mut self, + &self, _config_hash: &str, _emit_output_tx: Sender, ) { // No-op. } - async fn on_stop(&mut self) { + async fn on_stop(&self) { if let Some(handle) = &self.abort_handle { handle.abort(); } diff --git a/packages/desktop/src/providers/provider.rs b/packages/desktop/src/providers/provider.rs index 3f4ca313..499523c6 100644 --- a/packages/desktop/src/providers/provider.rs +++ b/packages/desktop/src/providers/provider.rs @@ -10,7 +10,7 @@ use tokio::{ use super::{provider_ref::ProviderOutput, variables::ProviderVariables}; #[async_trait] -pub trait Provider { +pub trait Provider: Send + Sync { /// Callback for when the provider is started. async fn on_start( &mut self, @@ -20,13 +20,13 @@ pub trait Provider { /// Callback for when the provider is refreshed. async fn on_refresh( - &mut self, + &self, config_hash: &str, emit_output_tx: Sender, ); /// Callback for when the provider is stopped. - async fn on_stop(&mut self); + async fn on_stop(&self); /// Minimum interval between refreshes. /// @@ -35,7 +35,7 @@ pub trait Provider { } #[async_trait] -pub trait IntervalProvider { +pub trait IntervalProvider: Send + Sync { type Config: Sync + Send + 'static + IntervalConfig; type State: Sync + Send + 'static; @@ -93,11 +93,10 @@ impl Provider for T { }); self.set_abort_handle(interval_task.abort_handle()); - _ = interval_task.await; } async fn on_refresh( - &mut self, + &self, config_hash: &str, emit_output_tx: Sender, ) { @@ -114,7 +113,7 @@ impl Provider for T { .await; } - async fn on_stop(&mut self) { + async fn on_stop(&self) { if let Some(handle) = &self.abort_handle() { handle.abort(); } diff --git a/packages/desktop/src/providers/provider_manager.rs b/packages/desktop/src/providers/provider_manager.rs index f007e81e..51f0c1f8 100644 --- a/packages/desktop/src/providers/provider_manager.rs +++ b/packages/desktop/src/providers/provider_manager.rs @@ -88,25 +88,27 @@ impl ProviderManager { config: ProviderConfig, _tracked_access: Vec, ) -> anyhow::Result<()> { - let found_provider = - { self.providers.lock().await.get(&config_hash).cloned() }; - - // If a provider with the given config already exists, refresh it - // and return early. - if let Some(found_provider) = found_provider { - if let Err(err) = found_provider.refresh().await { - warn!("Error refreshing provider: {:?}", err); - } + { + let mut providers = self.providers.lock().await; + + // If a provider with the given config already exists, refresh it + // and return early. + if let Some(found_provider) = providers.get_mut(&config_hash) { + if let Err(err) = found_provider.refresh().await { + warn!("Error refreshing provider: {:?}", err); + } - return Ok(()); - }; + return Ok(()); + }; + } let provider_ref = ProviderRef::new( config_hash.clone(), config, self.emit_output_tx.clone(), &self.shared_state, - )?; + ) + .await?; let mut providers = self.providers.lock().await; providers.insert(config_hash, provider_ref); diff --git a/packages/desktop/src/providers/provider_ref.rs b/packages/desktop/src/providers/provider_ref.rs index c94e1f48..a92baf4c 100644 --- a/packages/desktop/src/providers/provider_ref.rs +++ b/packages/desktop/src/providers/provider_ref.rs @@ -2,7 +2,7 @@ use std::time::{Duration, Instant}; use anyhow::bail; use serde::Serialize; -use tokio::{sync::mpsc, task}; +use tokio::sync::mpsc; use tracing::info; #[cfg(windows)] @@ -16,14 +16,12 @@ use super::{ }; /// Reference to an active provider. -#[derive(Debug, Clone)] pub struct ProviderRef { pub config_hash: String, pub min_refresh_interval: Option, pub cache: Option, pub emit_output_tx: mpsc::Sender, - pub refresh_tx: mpsc::Sender<()>, - pub stop_tx: mpsc::Sender<()>, + provider: Box, } /// Cache for provider output. @@ -64,80 +62,29 @@ impl From> for VariablesResult { impl ProviderRef { /// Creates a new `ProviderRef` instance. - pub fn new( + pub async fn new( config_hash: String, config: ProviderConfig, emit_output_tx: mpsc::Sender, shared_state: &SharedProviderState, ) -> anyhow::Result { - let provider = Self::create_provider(config, shared_state)?; + let mut provider = Self::create_provider(config, shared_state)?; let min_refresh_interval = provider.min_refresh_interval(); - let (refresh_tx, refresh_rx) = mpsc::channel::<()>(1); - let (stop_tx, stop_rx) = mpsc::channel::<()>(1); - - Self::start_provider( - provider, - config_hash.clone(), - emit_output_tx.clone(), - refresh_rx, - stop_rx, - ); + info!("Starting provider: {}", config_hash); + provider + .on_start(&config_hash, emit_output_tx.clone()) + .await; Ok(Self { config_hash, min_refresh_interval, cache: None, emit_output_tx, - refresh_tx, - stop_tx, + provider, }) } - /// Starts the provider in a separate task. - fn start_provider( - mut provider: Box, - config_hash: String, - emit_output_tx: mpsc::Sender, - mut refresh_rx: mpsc::Receiver<()>, - mut stop_rx: mpsc::Receiver<()>, - ) { - task::spawn(async move { - let mut has_started = false; - - // Loop to avoid exiting the select on refresh. - loop { - let config_hash = config_hash.clone(); - let emit_output_tx = emit_output_tx.clone(); - - tokio::select! { - // Default match arm which handles initialization of the provider. - // This has a precondition to avoid running again on refresh. - _ = { - info!("Starting provider: {}", config_hash); - has_started = true; - provider.on_start(&config_hash, emit_output_tx.clone()) - }, if !has_started => break, - - // On refresh, re-emit provider variables and continue looping. - Some(_) = refresh_rx.recv() => { - info!("Refreshing provider: {}", config_hash); - _ = provider.on_refresh(&config_hash, emit_output_tx).await; - }, - - // On stop, perform any necessary clean up and exit the loop. - Some(_) = stop_rx.recv() => { - info!("Stopping provider: {}", config_hash); - _ = provider.on_stop().await; - break; - }, - } - } - - info!("Provider stopped: {}", config_hash); - }); - } - fn create_provider( config: ProviderConfig, shared_state: &SharedProviderState, @@ -186,7 +133,7 @@ impl ProviderRef { /// /// Since the previous output of providers is cached, if within the /// minimum refresh interval, send the previous output. - pub async fn refresh(&self) -> anyhow::Result<()> { + pub async fn refresh(&mut self) -> anyhow::Result<()> { let min_refresh_interval = self.min_refresh_interval.unwrap_or(Duration::MAX); @@ -194,7 +141,13 @@ impl ProviderRef { Some(cache) if cache.timestamp.elapsed() >= min_refresh_interval => { self.emit_output_tx.send(*cache.output.clone()).await?; } - _ => self.refresh_tx.send(()).await?, + _ => { + info!("Refreshing provider: {}", self.config_hash); + self + .provider + .on_refresh(&self.config_hash, self.emit_output_tx.clone()) + .await; + } }; Ok(()) @@ -203,8 +156,10 @@ impl ProviderRef { /// Stops the given provider. /// /// This triggers any necessary cleanup. - pub async fn stop(&self) -> anyhow::Result<()> { - self.stop_tx.send(()).await?; + pub async fn stop(&mut self) -> anyhow::Result<()> { + info!("Stopping provider: {}", self.config_hash); + _ = self.provider.on_stop().await; + info!("Provider stopped: {}", self.config_hash); Ok(()) } diff --git a/packages/desktop/src/sys_tray.rs b/packages/desktop/src/sys_tray.rs index d95c9d75..4e392ddf 100644 --- a/packages/desktop/src/sys_tray.rs +++ b/packages/desktop/src/sys_tray.rs @@ -7,6 +7,7 @@ use tauri::{ tray::{TrayIcon, TrayIconBuilder}, AppHandle, Wry, }; +use tokio::task; use tracing::{error, info}; use crate::{ @@ -83,6 +84,7 @@ impl SysTray { async fn create_tray_icon(&self) -> anyhow::Result { let config = self.config.clone(); + let window_factory = self.window_factory.clone(); let tooltip = format!("Zebar v{}", env!("VERSION_NUMBER")); let tray_icon = TrayIconBuilder::with_id("tray") @@ -93,7 +95,12 @@ impl SysTray { let event = MenuEvent::from_str(event.id.as_ref()); let event_res = event.map(|event| { - Self::handle_menu_event(event, &app, config.clone()) + Self::handle_menu_event( + event, + &app, + config.clone(), + window_factory.clone(), + ) }); if let Err(err) = event_res { @@ -107,7 +114,6 @@ impl SysTray { fn start_listener(self: Arc) { let mut config_changes_rx = self.config.changes_tx.subscribe(); - // TODO: Add `changes_tx` to `WindowFactory`. let mut window_changes_rx = self.window_factory.changes_tx.subscribe(); tokio::spawn(async move { @@ -174,6 +180,7 @@ impl SysTray { event: MenuEvent, app_handle: &AppHandle, config: Arc, + window_factory: Arc, ) -> anyhow::Result<()> { match event { MenuEvent::ShowConfigFolder => { @@ -188,11 +195,19 @@ impl SysTray { app_handle.exit(0) } - MenuEvent::EnableWindowConfig(id) => { - info!("Window config at index {} enabled.", id); + MenuEvent::EnableWindowConfig(path) => { + info!("Window config at path {} enabled.", path); + + // task::spawn(async move { + // window_factory.open(path).await; + // }) } - MenuEvent::StartupWindowConfig(id) => { - info!("Window config at index {} set to launch on startup.", id); + MenuEvent::StartupWindowConfig(path) => { + info!("Window config at path {} set to launch on startup.", path); + + // task::spawn(async move { + // config.add_startup_config(path).await; + // }) } }; @@ -244,6 +259,7 @@ impl SysTray { None::<&str>, )?; + // ** // TODO: Get whether it's launched on startup. let startup_item = CheckMenuItem::with_id( &self.app_handle, From 95be10eba802366fe57d9d7702425773c42a4fe0 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 5 Sep 2024 21:18:18 +0800 Subject: [PATCH 081/138] refactor: remove `IntervalProvider` trait; remove `ProviderManager::init` --- examples/solidjs-ts/src/index.tsx | 18 +-- .../battery/create-battery-provider.ts | 2 +- .../desktop/src/providers/battery/config.rs | 4 - .../desktop/src/providers/battery/provider.rs | 74 ++++----- packages/desktop/src/providers/config.rs | 27 ++-- packages/desktop/src/providers/cpu/config.rs | 4 +- .../desktop/src/providers/cpu/provider.rs | 89 +++++------ .../src/providers/komorebi/provider.rs | 95 ++++------- packages/desktop/src/providers/mod.rs | 14 +- packages/desktop/src/providers/provider.rs | 136 ++-------------- .../desktop/src/providers/provider_manager.rs | 66 ++------ .../desktop/src/providers/provider_ref.rs | 150 +++++++++++------- packages/desktop/src/providers/variables.rs | 26 +-- 13 files changed, 259 insertions(+), 446 deletions(-) diff --git a/examples/solidjs-ts/src/index.tsx b/examples/solidjs-ts/src/index.tsx index c19cd189..0b6f6bd5 100644 --- a/examples/solidjs-ts/src/index.tsx +++ b/examples/solidjs-ts/src/index.tsx @@ -6,11 +6,11 @@ import { init } from 'zebar'; import { createEffect } from 'solid-js'; const zebarCtx = await init(); -const [cpu, battery, memory, weather] = await Promise.all([ +const [cpu, battery] = await Promise.all([ zebarCtx.createProvider({ type: 'cpu' }), zebarCtx.createProvider({ type: 'battery' }), - zebarCtx.createProvider({ type: 'memory' }), - zebarCtx.createProvider({ type: 'weather' }), + // zebarCtx.createProvider({ type: 'memory' }), + // zebarCtx.createProvider({ type: 'weather' }), ]); render(() => , document.getElementById('root')!); @@ -19,22 +19,22 @@ function App() { const [store, setStore] = createStore({ cpu: cpu.output, battery: battery.output, - memory: memory.output, - weather: weather.output, + // memory: memory.output, + // weather: weather.output, }); cpu.onOutput(cpu => setStore({ cpu })); battery.onOutput(battery => setStore({ battery })); - memory.onOutput(memory => setStore({ memory })); - weather.onOutput(weather => setStore({ weather })); + // memory.onOutput(memory => setStore({ memory })); + // weather.onOutput(weather => setStore({ weather })); return (
cpu: {store.cpu.usage} battery: {store.battery?.chargePercent} - memory: {store.memory.usage} + {/* memory: {store.memory.usage} weather temp: {store.weather.celsiusTemp} - weather status: {store.weather.status} + weather status: {store.weather.status} */}
); } diff --git a/packages/client-api/src/providers/battery/create-battery-provider.ts b/packages/client-api/src/providers/battery/create-battery-provider.ts index 77bbe914..ace2f55e 100644 --- a/packages/client-api/src/providers/battery/create-battery-provider.ts +++ b/packages/client-api/src/providers/battery/create-battery-provider.ts @@ -17,7 +17,7 @@ export interface BatteryProviderConfig { const batteryProviderConfigSchema = z.object({ type: z.literal('battery'), - refreshInterval: z.coerce.number().default(60 * 60 * 1000), + refreshInterval: z.coerce.number().default(60 * 1000), }); export type BatteryProvider = Provider< diff --git a/packages/desktop/src/providers/battery/config.rs b/packages/desktop/src/providers/battery/config.rs index cb3c8084..85da02f8 100644 --- a/packages/desktop/src/providers/battery/config.rs +++ b/packages/desktop/src/providers/battery/config.rs @@ -1,11 +1,7 @@ use serde::Deserialize; -use crate::impl_interval_config; - #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct BatteryProviderConfig { pub refresh_interval: u64, } - -impl_interval_config!(BatteryProviderConfig); diff --git a/packages/desktop/src/providers/battery/provider.rs b/packages/desktop/src/providers/battery/provider.rs index 942fa365..2af5cea5 100644 --- a/packages/desktop/src/providers/battery/provider.rs +++ b/packages/desktop/src/providers/battery/provider.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{sync::Arc, time::Duration}; use anyhow::Context; use async_trait::async_trait; @@ -9,42 +9,43 @@ use starship_battery::{ }, Manager, State, }; -use tokio::task::AbortHandle; +use tokio::{sync::mpsc, time}; use super::{BatteryProviderConfig, BatteryVariables}; use crate::providers::{ - provider::IntervalProvider, variables::ProviderVariables, + provider::Provider, + provider_manager::SharedProviderState, + provider_ref::{ProviderOutput, VariablesResult}, + variables::ProviderVariables, }; pub struct BatteryProvider { - pub config: Arc, - abort_handle: Option, - battery_manager: Arc, + config: BatteryProviderConfig, + battery_manager: Manager, } impl BatteryProvider { pub fn new( config: BatteryProviderConfig, ) -> anyhow::Result { - let manager = Manager::new()?; - Ok(BatteryProvider { - config: Arc::new(config), - abort_handle: None, - battery_manager: Arc::new(manager), + config, + battery_manager: Manager::new()?, }) } /// Battery manager from `starship_battery` is not thread-safe, so it /// requires its own non-async function. - fn get_variables(manager: &Manager) -> anyhow::Result { - let first_battery = manager + pub fn get_variables( + manager: &Manager, + ) -> anyhow::Result { + let battery = manager .batteries() .and_then(|mut batteries| batteries.nth(0).transpose()) .unwrap_or(None) - .context("No battery found."); + .context("No battery found.")?; - first_battery.map(|battery| BatteryVariables { + Ok(ProviderVariables::Battery(BatteryVariables { charge_percent: battery.state_of_charge().get::(), health_percent: battery.state_of_health().get::(), state: battery.state().to_string(), @@ -58,37 +59,26 @@ impl BatteryProvider { power_consumption: battery.energy_rate().get::(), voltage: battery.voltage().get::(), cycle_count: battery.cycle_count(), - }) + })) } } #[async_trait] -impl IntervalProvider for BatteryProvider { - type Config = BatteryProviderConfig; - type State = Manager; - - fn config(&self) -> Arc { - self.config.clone() - } - - fn state(&self) -> Arc { - self.battery_manager.clone() - } - - fn abort_handle(&self) -> &Option { - &self.abort_handle - } +impl Provider for BatteryProvider { + async fn on_start( + self: Arc, + shared_state: SharedProviderState, + emit_output_tx: mpsc::Sender, + ) { + let mut interval = + time::interval(Duration::from_millis(self.config.refresh_interval)); - fn set_abort_handle(&mut self, abort_handle: AbortHandle) { - self.abort_handle = Some(abort_handle) - } - - async fn get_refreshed_variables( - _: &BatteryProviderConfig, - battery_manager: &Manager, - ) -> anyhow::Result { - Ok(ProviderVariables::Battery(Self::get_variables( - battery_manager, - )?)) + loop { + interval.tick().await; + emit_output_tx + .send(Self::get_variables(&self.battery_manager).into()) + .await + .unwrap(); + } } } diff --git a/packages/desktop/src/providers/config.rs b/packages/desktop/src/providers/config.rs index 40da9c9a..e4b4c83e 100644 --- a/packages/desktop/src/providers/config.rs +++ b/packages/desktop/src/providers/config.rs @@ -1,12 +1,13 @@ use serde::Deserialize; -#[cfg(windows)] -use super::komorebi::KomorebiProviderConfig; +// #[cfg(windows)] +// use super::komorebi::KomorebiProviderConfig; use super::{ - battery::BatteryProviderConfig, cpu::CpuProviderConfig, - host::HostProviderConfig, ip::IpProviderConfig, - memory::MemoryProviderConfig, network::NetworkProviderConfig, - weather::WeatherProviderConfig, + battery::BatteryProviderConfig, + cpu::CpuProviderConfig, + // host::HostProviderConfig, ip::IpProviderConfig, + // memory::MemoryProviderConfig, network::NetworkProviderConfig, + // weather::WeatherProviderConfig, }; #[derive(Deserialize, Debug)] @@ -14,11 +15,11 @@ use super::{ pub enum ProviderConfig { Battery(BatteryProviderConfig), Cpu(CpuProviderConfig), - Host(HostProviderConfig), - Ip(IpProviderConfig), - #[cfg(windows)] - Komorebi(KomorebiProviderConfig), - Memory(MemoryProviderConfig), - Network(NetworkProviderConfig), - Weather(WeatherProviderConfig), + // Host(HostProviderConfig), + // Ip(IpProviderConfig), + // #[cfg(windows)] + // Komorebi(KomorebiProviderConfig), + // Memory(MemoryProviderConfig), + // Network(NetworkProviderConfig), + // Weather(WeatherProviderConfig), } diff --git a/packages/desktop/src/providers/cpu/config.rs b/packages/desktop/src/providers/cpu/config.rs index 21afadb6..a0d0ccd3 100644 --- a/packages/desktop/src/providers/cpu/config.rs +++ b/packages/desktop/src/providers/cpu/config.rs @@ -1,6 +1,6 @@ use serde::Deserialize; -use crate::impl_interval_config; +// use crate::impl_interval_config; #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] @@ -8,4 +8,4 @@ pub struct CpuProviderConfig { pub refresh_interval: u64, } -impl_interval_config!(CpuProviderConfig); +// impl_interval_config!(CpuProviderConfig); diff --git a/packages/desktop/src/providers/cpu/provider.rs b/packages/desktop/src/providers/cpu/provider.rs index 9af50d1a..d4ab3d92 100644 --- a/packages/desktop/src/providers/cpu/provider.rs +++ b/packages/desktop/src/providers/cpu/provider.rs @@ -1,69 +1,56 @@ -use std::sync::Arc; +use std::{sync::Arc, time::Duration}; use async_trait::async_trait; use sysinfo::System; -use tokio::{sync::Mutex, task::AbortHandle}; +use tokio::{ + sync::{mpsc, Mutex}, + task::AbortHandle, + time, +}; use super::{CpuProviderConfig, CpuVariables}; use crate::providers::{ - provider::IntervalProvider, variables::ProviderVariables, + provider::Provider, provider_manager::SharedProviderState, + provider_ref::VariablesResult, variables::ProviderVariables, }; pub struct CpuProvider { - pub config: Arc, - abort_handle: Option, - sysinfo: Arc>, + config: CpuProviderConfig, } impl CpuProvider { - pub fn new( - config: CpuProviderConfig, - sysinfo: Arc>, - ) -> CpuProvider { - CpuProvider { - config: Arc::new(config), - abort_handle: None, - sysinfo, - } + pub fn new(config: CpuProviderConfig) -> CpuProvider { + CpuProvider { config } } } #[async_trait] -impl IntervalProvider for CpuProvider { - type Config = CpuProviderConfig; - type State = Mutex; - - fn config(&self) -> Arc { - self.config.clone() - } - - fn state(&self) -> Arc> { - self.sysinfo.clone() - } - - fn abort_handle(&self) -> &Option { - &self.abort_handle - } - - fn set_abort_handle(&mut self, abort_handle: AbortHandle) { - self.abort_handle = Some(abort_handle) - } - - async fn get_refreshed_variables( - _: &CpuProviderConfig, - sysinfo: &Mutex, - ) -> anyhow::Result { - let mut sysinfo = sysinfo.lock().await; - sysinfo.refresh_cpu(); - - Ok(ProviderVariables::Cpu(CpuVariables { - usage: sysinfo.global_cpu_info().cpu_usage(), - frequency: sysinfo.global_cpu_info().frequency(), - logical_core_count: sysinfo.cpus().len(), - physical_core_count: sysinfo - .physical_core_count() - .unwrap_or(sysinfo.cpus().len()), - vendor: sysinfo.global_cpu_info().vendor_id().into(), - })) +impl Provider for CpuProvider { + async fn on_start( + self: Arc, + shared_state: SharedProviderState, + emit_output_tx: mpsc::Sender, + ) { + let mut interval = + time::interval(Duration::from_millis(self.config.refresh_interval)); + + loop { + interval.tick().await; + + let mut sysinfo = shared_state.sysinfo.lock().await; + sysinfo.refresh_cpu(); + + let res = Ok(ProviderVariables::Cpu(CpuVariables { + usage: sysinfo.global_cpu_info().cpu_usage(), + frequency: sysinfo.global_cpu_info().frequency(), + logical_core_count: sysinfo.cpus().len(), + physical_core_count: sysinfo + .physical_core_count() + .unwrap_or(sysinfo.cpus().len()), + vendor: sysinfo.global_cpu_info().vendor_id().into(), + })); + + emit_output_tx.send(res.into()).await; + } } } diff --git a/packages/desktop/src/providers/komorebi/provider.rs b/packages/desktop/src/providers/komorebi/provider.rs index a13e33f4..11784e6f 100644 --- a/packages/desktop/src/providers/komorebi/provider.rs +++ b/packages/desktop/src/providers/komorebi/provider.rs @@ -6,10 +6,7 @@ use std::{ use async_trait::async_trait; use komorebi_client::{Container, Monitor, Window, Workspace}; -use tokio::{ - sync::mpsc::Sender, - task::{self, AbortHandle}, -}; +use tokio::{sync::mpsc::Sender, task::AbortHandle}; use tracing::debug; use super::{ @@ -123,11 +120,6 @@ impl KomorebiProvider { #[async_trait] impl Provider for KomorebiProvider { - fn min_refresh_interval(&self) -> Option { - // State should always be up to date. - None - } - async fn on_start( &mut self, config_hash: &str, @@ -135,62 +127,43 @@ impl Provider for KomorebiProvider { ) { let config_hash = config_hash.to_string(); - let task_handle = task::spawn(async move { - let socket = komorebi_client::subscribe(SOCKET_NAME).unwrap(); - debug!("Connected to Komorebi socket."); - - for incoming in socket.incoming() { - debug!("Incoming Komorebi socket message."); - - match incoming { - Ok(data) => { - let reader = BufReader::new(data.try_clone().unwrap()); - - for line in reader.lines().flatten() { - if let Ok(notification) = serde_json::from_str::< - komorebi_client::Notification, - >(&line) - { - // Transform and emit the incoming Komorebi state. - _ = emit_output_tx - .send(ProviderOutput { - config_hash: config_hash.clone(), - variables: VariablesResult::Data( - ProviderVariables::Komorebi( - Self::transform_response(notification.state), - ), - ), - }) - .await; - } + let socket = komorebi_client::subscribe(SOCKET_NAME).unwrap(); + debug!("Connected to Komorebi socket."); + + for incoming in socket.incoming() { + debug!("Incoming Komorebi socket message."); + + match incoming { + Ok(data) => { + let reader = BufReader::new(data.try_clone().unwrap()); + + for line in reader.lines().flatten() { + if let Ok(notification) = + serde_json::from_str::(&line) + { + // Transform and emit the incoming Komorebi state. + _ = emit_output_tx + .send(ProviderOutput { + config_hash: config_hash.clone(), + variables: VariablesResult::Data( + ProviderVariables::Komorebi(Self::transform_response( + notification.state, + )), + ), + }) + .await; } } - Err(error) => { - _ = emit_output_tx - .send(ProviderOutput { - config_hash: config_hash.to_string(), - variables: VariablesResult::Error(error.to_string()), - }) - .await; - } + } + Err(error) => { + _ = emit_output_tx + .send(ProviderOutput { + config_hash: config_hash.to_string(), + variables: VariablesResult::Error(error.to_string()), + }) + .await; } } - }); - - self.abort_handle = Some(task_handle.abort_handle()); - } - - async fn on_refresh( - &self, - _config_hash: &str, - _emit_output_tx: Sender, - ) { - // No-op. - } - - async fn on_stop(&self) { - if let Some(handle) = &self.abort_handle { - handle.abort(); } } } diff --git a/packages/desktop/src/providers/mod.rs b/packages/desktop/src/providers/mod.rs index b902a359..75f79edc 100644 --- a/packages/desktop/src/providers/mod.rs +++ b/packages/desktop/src/providers/mod.rs @@ -1,14 +1,14 @@ pub mod battery; pub mod config; pub mod cpu; -pub mod host; -pub mod ip; -#[cfg(windows)] -pub mod komorebi; -pub mod memory; -pub mod network; +// pub mod host; +// pub mod ip; +// #[cfg(windows)] +// pub mod komorebi; +// pub mod memory; +// pub mod network; pub mod provider; pub mod provider_manager; pub mod provider_ref; pub mod variables; -pub mod weather; +// pub mod weather; diff --git a/packages/desktop/src/providers/provider.rs b/packages/desktop/src/providers/provider.rs index 499523c6..6ff8122e 100644 --- a/packages/desktop/src/providers/provider.rs +++ b/packages/desktop/src/providers/provider.rs @@ -1,139 +1,23 @@ -use std::{sync::Arc, time::Duration}; +use std::sync::Arc; use async_trait::async_trait; -use tokio::{ - sync::mpsc::Sender, - task::{self, AbortHandle}, - time, -}; +use tokio::sync::mpsc::Sender; -use super::{provider_ref::ProviderOutput, variables::ProviderVariables}; +use super::{ + provider_manager::SharedProviderState, provider_ref::VariablesResult, +}; #[async_trait] pub trait Provider: Send + Sync { /// Callback for when the provider is started. async fn on_start( - &mut self, - config_hash: &str, - emit_output_tx: Sender, - ); - - /// Callback for when the provider is refreshed. - async fn on_refresh( - &self, - config_hash: &str, - emit_output_tx: Sender, + self: Arc, + shared_state: SharedProviderState, + emit_output_tx: Sender, ); /// Callback for when the provider is stopped. - async fn on_stop(&self); - - /// Minimum interval between refreshes. - /// - /// Affects how the provider output is cached. - fn min_refresh_interval(&self) -> Option; -} - -#[async_trait] -pub trait IntervalProvider: Send + Sync { - type Config: Sync + Send + 'static + IntervalConfig; - type State: Sync + Send + 'static; - - /// Default to 2 seconds as the minimum refresh interval. - fn min_refresh_interval(&self) -> Option { - Some(Duration::from_secs(2)) - } - - fn config(&self) -> Arc; - - fn state(&self) -> Arc; - - fn abort_handle(&self) -> &Option; - - fn set_abort_handle(&mut self, abort_handle: AbortHandle); - - async fn get_refreshed_variables( - config: &Self::Config, - state: &Self::State, - ) -> anyhow::Result; -} - -#[async_trait] -impl Provider for T { - fn min_refresh_interval(&self) -> Option { - T::min_refresh_interval(self) + async fn on_stop(self: Arc) { + // No-op by default. } - - async fn on_start( - &mut self, - config_hash: &str, - emit_output_tx: Sender, - ) { - let config = self.config(); - let state = self.state(); - let config_hash = config_hash.to_string(); - - let interval_task = task::spawn(async move { - let mut interval = - time::interval(Duration::from_millis(config.refresh_interval())); - - loop { - // The first tick fires immediately. - interval.tick().await; - - _ = emit_output_tx - .send(ProviderOutput { - config_hash: config_hash.clone(), - variables: T::get_refreshed_variables(&config, &state) - .await - .into(), - }) - .await; - } - }); - - self.set_abort_handle(interval_task.abort_handle()); - } - - async fn on_refresh( - &self, - config_hash: &str, - emit_output_tx: Sender, - ) { - _ = emit_output_tx - .send(ProviderOutput { - config_hash: config_hash.to_string(), - variables: T::get_refreshed_variables( - &self.config(), - &self.state(), - ) - .await - .into(), - }) - .await; - } - - async fn on_stop(&self) { - if let Some(handle) = &self.abort_handle() { - handle.abort(); - } - } -} - -/// Require interval providers to have a refresh interval in their config. -pub trait IntervalConfig { - fn refresh_interval(&self) -> u64; -} - -#[macro_export] -macro_rules! impl_interval_config { - ($struct_name:ident) => { - use crate::providers::provider::IntervalConfig; - - impl IntervalConfig for $struct_name { - fn refresh_interval(&self) -> u64 { - self.refresh_interval - } - } - }; } diff --git a/packages/desktop/src/providers/provider_manager.rs b/packages/desktop/src/providers/provider_manager.rs index 51f0c1f8..20db3b67 100644 --- a/packages/desktop/src/providers/provider_manager.rs +++ b/packages/desktop/src/providers/provider_manager.rs @@ -1,22 +1,14 @@ use std::{collections::HashMap, sync::Arc}; use sysinfo::{Networks, System}; -use tauri::{AppHandle, Emitter}; -use tokio::{ - sync::{ - mpsc::{self}, - Mutex, - }, - task, -}; -use tracing::{info, warn}; +use tauri::AppHandle; +use tokio::sync::Mutex; +use tracing::warn; -use super::{ - config::ProviderConfig, - provider_ref::{ProviderOutput, ProviderRef}, -}; +use super::{config::ProviderConfig, provider_ref::ProviderRef}; /// State shared between providers. +#[derive(Clone)] pub struct SharedProviderState { pub sysinfo: Arc>, pub netinfo: Arc>, @@ -24,22 +16,16 @@ pub struct SharedProviderState { /// Manages the creation and cleanup of providers. pub struct ProviderManager { - emit_output_tx: mpsc::Sender, + app_handle: AppHandle, providers: Arc>>, shared_state: SharedProviderState, } impl ProviderManager { pub fn new(app_handle: &AppHandle) -> Self { - let (emit_output_tx, emit_output_rx) = - mpsc::channel::(1); - - let providers = Arc::new(Mutex::new(HashMap::new())); - Self::start_listener(app_handle, emit_output_rx, providers.clone()); - Self { - emit_output_tx, - providers, + app_handle: app_handle.clone(), + providers: Arc::new(Mutex::new(HashMap::new())), shared_state: SharedProviderState { sysinfo: Arc::new(Mutex::new(System::new_all())), netinfo: Arc::new(Mutex::new(Networks::new_with_refreshed_list())), @@ -47,40 +33,6 @@ impl ProviderManager { } } - /// Starts listening for provider outputs and emits them to frontend - /// clients. - fn start_listener( - app_handle: &AppHandle, - mut emit_output_rx: mpsc::Receiver, - providers: Arc>>, - ) { - let app_handle = app_handle.clone(); - - task::spawn(async move { - while let Some(output) = emit_output_rx.recv().await { - info!("Emitting for provider: {}", output.config_hash); - - let output = Box::new(output); - - if let Err(err) = app_handle.emit("provider-emit", output.clone()) - { - warn!("Error emitting provider output: {:?}", err); - } - - // Update the provider's output cache. - if let Ok(mut providers) = providers.try_lock() { - if let Some(found_provider) = - providers.get_mut(&output.config_hash) - { - found_provider.update_cache(output); - } - } else { - warn!("Failed to update provider output cache."); - } - } - }); - } - /// Creates a provider with the given config. pub async fn create( &self, @@ -103,9 +55,9 @@ impl ProviderManager { } let provider_ref = ProviderRef::new( + &self.app_handle, config_hash.clone(), config, - self.emit_output_tx.clone(), &self.shared_state, ) .await?; diff --git a/packages/desktop/src/providers/provider_ref.rs b/packages/desktop/src/providers/provider_ref.rs index a92baf4c..ea44197b 100644 --- a/packages/desktop/src/providers/provider_ref.rs +++ b/packages/desktop/src/providers/provider_ref.rs @@ -1,27 +1,34 @@ -use std::time::{Duration, Instant}; +use std::{ + sync::Arc, + time::{Duration, Instant}, +}; use anyhow::bail; use serde::Serialize; -use tokio::sync::mpsc; -use tracing::info; +use tauri::{AppHandle, Emitter}; +use tokio::{sync::mpsc, task}; +use tracing::{info, warn}; -#[cfg(windows)] -use super::komorebi::KomorebiProvider; +// #[cfg(windows)] +// use super::komorebi::KomorebiProvider; use super::{ - battery::BatteryProvider, config::ProviderConfig, cpu::CpuProvider, - host::HostProvider, ip::IpProvider, memory::MemoryProvider, - network::NetworkProvider, provider::Provider, - provider_manager::SharedProviderState, variables::ProviderVariables, - weather::WeatherProvider, + battery::BatteryProvider, + config::ProviderConfig, + cpu::CpuProvider, + // host::HostProvider, ip::IpProvider, memory::MemoryProvider, + // network::NetworkProvider, + provider::Provider, + provider_manager::SharedProviderState, + variables::ProviderVariables, + // weather::WeatherProvider, }; /// Reference to an active provider. pub struct ProviderRef { pub config_hash: String, - pub min_refresh_interval: Option, pub cache: Option, - pub emit_output_tx: mpsc::Sender, - provider: Box, + provider: Arc, + emit_output_tx: mpsc::Sender, } /// Cache for provider output. @@ -63,57 +70,90 @@ impl From> for VariablesResult { impl ProviderRef { /// Creates a new `ProviderRef` instance. pub async fn new( + app_handle: &AppHandle, config_hash: String, config: ProviderConfig, - emit_output_tx: mpsc::Sender, shared_state: &SharedProviderState, ) -> anyhow::Result { - let mut provider = Self::create_provider(config, shared_state)?; - let min_refresh_interval = provider.min_refresh_interval(); + let provider = Self::create_provider(config, shared_state)?; + + let (emit_output_tx, mut emit_output_rx) = + mpsc::channel::(1); + + let config_hash_clone = config_hash.clone(); + let app_handle = app_handle.clone(); + task::spawn(async move { + while let Some(output) = emit_output_rx.recv().await { + info!("Emitting for provider: {}", config_hash_clone); + + let output = Box::new(output); + let xx = ProviderOutput { + config_hash: config_hash_clone.clone(), + variables: *output.clone(), + }; + + if let Err(err) = app_handle.emit("provider-emit", xx) { + warn!("Error emitting provider output: {:?}", err); + } + + // Update the provider's output cache. + // if let Ok(mut providers) = providers.try_lock() { + // if let Some(found_provider) = + // providers.get_mut(&output.config_hash) + // { + // found_provider.update_cache(output); + // } + // } else { + // warn!("Failed to update provider output cache."); + // } + } + }); info!("Starting provider: {}", config_hash); - provider - .on_start(&config_hash, emit_output_tx.clone()) - .await; + let provider_clone = provider.clone(); + let shared_state = shared_state.clone(); + let emit_output_tx_clone = emit_output_tx.clone(); + task::spawn(async move { + provider_clone + .on_start(shared_state.clone(), emit_output_tx_clone) + .await; + }); Ok(Self { config_hash, - min_refresh_interval, cache: None, - emit_output_tx, provider, + emit_output_tx, }) } fn create_provider( config: ProviderConfig, shared_state: &SharedProviderState, - ) -> anyhow::Result> { - let provider: Box = match config { + ) -> anyhow::Result> { + let provider: Arc = match config { ProviderConfig::Battery(config) => { - Box::new(BatteryProvider::new(config)?) - } - ProviderConfig::Cpu(config) => { - Box::new(CpuProvider::new(config, shared_state.sysinfo.clone())) - } - ProviderConfig::Host(config) => { - Box::new(HostProvider::new(config, shared_state.sysinfo.clone())) - } - ProviderConfig::Ip(config) => Box::new(IpProvider::new(config)), - #[cfg(windows)] - ProviderConfig::Komorebi(config) => { - Box::new(KomorebiProvider::new(config)) - } - ProviderConfig::Memory(config) => { - Box::new(MemoryProvider::new(config, shared_state.sysinfo.clone())) - } - ProviderConfig::Network(config) => Box::new(NetworkProvider::new( - config, - shared_state.netinfo.clone(), - )), - ProviderConfig::Weather(config) => { - Box::new(WeatherProvider::new(config)) + Arc::new(BatteryProvider::new(config)?) } + ProviderConfig::Cpu(config) => Arc::new(CpuProvider::new(config)), + // ProviderConfig::Host(config) => { + // Box::new(HostProvider::new(config, + // shared_state.sysinfo.clone())) } + // ProviderConfig::Ip(config) => Box::new(IpProvider::new(config)), + // #[cfg(windows)] + // ProviderConfig::Komorebi(config) => { + // Box::new(KomorebiProvider::new(config)) + // } + // ProviderConfig::Memory(config) => { + // Box::new(MemoryProvider::new(config, + // shared_state.sysinfo.clone())) } + // ProviderConfig::Network(config) => Box::new(NetworkProvider::new( + // config, + // shared_state.netinfo.clone(), + // )), + // ProviderConfig::Weather(config) => { + // Box::new(WeatherProvider::new(config)) + // } #[allow(unreachable_patterns)] _ => bail!("Provider not supported on this operating system."), }; @@ -134,21 +174,9 @@ impl ProviderRef { /// Since the previous output of providers is cached, if within the /// minimum refresh interval, send the previous output. pub async fn refresh(&mut self) -> anyhow::Result<()> { - let min_refresh_interval = - self.min_refresh_interval.unwrap_or(Duration::MAX); - - match &self.cache { - Some(cache) if cache.timestamp.elapsed() >= min_refresh_interval => { - self.emit_output_tx.send(*cache.output.clone()).await?; - } - _ => { - info!("Refreshing provider: {}", self.config_hash); - self - .provider - .on_refresh(&self.config_hash, self.emit_output_tx.clone()) - .await; - } - }; + // if let Some(cache) = self.cache { + // self.emit_output_tx.send(*cache.output.clone()).await?; + // } Ok(()) } @@ -158,7 +186,7 @@ impl ProviderRef { /// This triggers any necessary cleanup. pub async fn stop(&mut self) -> anyhow::Result<()> { info!("Stopping provider: {}", self.config_hash); - _ = self.provider.on_stop().await; + _ = self.provider.clone().on_stop().await; info!("Provider stopped: {}", self.config_hash); Ok(()) diff --git a/packages/desktop/src/providers/variables.rs b/packages/desktop/src/providers/variables.rs index e6337f92..2f9c9cdf 100644 --- a/packages/desktop/src/providers/variables.rs +++ b/packages/desktop/src/providers/variables.rs @@ -1,11 +1,13 @@ use serde::Serialize; -#[cfg(windows)] -use super::komorebi::KomorebiVariables; +// #[cfg(windows)] +// use super::komorebi::KomorebiVariables; use super::{ - battery::BatteryVariables, cpu::CpuVariables, host::HostVariables, - ip::IpVariables, memory::MemoryVariables, network::NetworkVariables, - weather::WeatherVariables, + battery::BatteryVariables, + cpu::CpuVariables, + // host::HostVariables, + // ip::IpVariables, memory::MemoryVariables, network::NetworkVariables, + // weather::WeatherVariables, }; #[derive(Serialize, Debug, Clone)] @@ -13,11 +15,11 @@ use super::{ pub enum ProviderVariables { Battery(BatteryVariables), Cpu(CpuVariables), - Host(HostVariables), - Ip(IpVariables), - #[cfg(windows)] - Komorebi(KomorebiVariables), - Memory(MemoryVariables), - Network(NetworkVariables), - Weather(WeatherVariables), + // Host(HostVariables), + // Ip(IpVariables), + // #[cfg(windows)] + // Komorebi(KomorebiVariables), + // Memory(MemoryVariables), + // Network(NetworkVariables), + // Weather(WeatherVariables), } From b7b875947f6f2646592ce27b7e45f50504ecccec Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 5 Sep 2024 21:45:26 +0800 Subject: [PATCH 082/138] refactor: variable renames --- .../client-api/src/desktop/desktop-events.ts | 2 +- .../battery/create-battery-provider.ts | 8 +-- .../src/providers/cpu/create-cpu-provider.ts | 8 +-- .../providers/host/create-host-provider.ts | 8 +-- .../src/providers/ip/create-ip-provider.ts | 8 +-- .../komorebi/create-komorebi-provider.ts | 8 +-- .../memory/create-memory-provider.ts | 8 +-- .../network/create-network-provider.ts | 8 +-- .../weather/create-weather-provider.ts | 8 +-- .../desktop/src/providers/battery/provider.rs | 17 ++--- .../src/providers/battery/variables.rs | 2 +- .../desktop/src/providers/cpu/provider.rs | 24 ++++--- .../desktop/src/providers/cpu/variables.rs | 2 +- .../desktop/src/providers/host/provider.rs | 8 +-- .../desktop/src/providers/host/variables.rs | 2 +- packages/desktop/src/providers/ip/provider.rs | 8 +-- .../desktop/src/providers/ip/variables.rs | 2 +- .../src/providers/komorebi/provider.rs | 28 +++----- .../src/providers/komorebi/variables.rs | 2 +- .../desktop/src/providers/memory/provider.rs | 8 +-- .../desktop/src/providers/memory/variables.rs | 2 +- .../desktop/src/providers/network/provider.rs | 10 +-- .../src/providers/network/variables.rs | 2 +- packages/desktop/src/providers/provider.rs | 7 +- .../desktop/src/providers/provider_manager.rs | 2 + .../desktop/src/providers/provider_ref.rs | 69 ++++++++----------- packages/desktop/src/providers/variables.rs | 30 ++++---- .../desktop/src/providers/weather/provider.rs | 8 +-- .../src/providers/weather/variables.rs | 2 +- 29 files changed, 141 insertions(+), 160 deletions(-) diff --git a/packages/client-api/src/desktop/desktop-events.ts b/packages/client-api/src/desktop/desktop-events.ts index e8585451..5e3f90dd 100644 --- a/packages/client-api/src/desktop/desktop-events.ts +++ b/packages/client-api/src/desktop/desktop-events.ts @@ -19,7 +19,7 @@ let callbacks: { export interface ProviderEmitEvent { configHash: string; - variables: { data: T } | { error: string }; + result: { output: T } | { error: string }; } /** diff --git a/packages/client-api/src/providers/battery/create-battery-provider.ts b/packages/client-api/src/providers/battery/create-battery-provider.ts index ace2f55e..d1b43807 100644 --- a/packages/client-api/src/providers/battery/create-battery-provider.ts +++ b/packages/client-api/src/providers/battery/create-battery-provider.ts @@ -43,11 +43,11 @@ export async function createBatteryProvider( const mergedConfig = batteryProviderConfigSchema.parse(config); return createBaseProvider(mergedConfig, async queue => { - return onProviderEmit(mergedConfig, ({ variables }) => { - if ('error' in variables) { - queue.error(variables.error); + return onProviderEmit(mergedConfig, ({ result }) => { + if ('error' in result) { + queue.error(result.error); } else { - queue.output(variables.data); + queue.output(result.output); } }); }); diff --git a/packages/client-api/src/providers/cpu/create-cpu-provider.ts b/packages/client-api/src/providers/cpu/create-cpu-provider.ts index 5a604ea6..9716e2bb 100644 --- a/packages/client-api/src/providers/cpu/create-cpu-provider.ts +++ b/packages/client-api/src/providers/cpu/create-cpu-provider.ts @@ -36,11 +36,11 @@ export async function createCpuProvider( const mergedConfig = cpuProviderConfigSchema.parse(config); return createBaseProvider(mergedConfig, async queue => { - return onProviderEmit(mergedConfig, ({ variables }) => { - if ('error' in variables) { - queue.error(variables.error); + return onProviderEmit(mergedConfig, ({ result }) => { + if ('error' in result) { + queue.error(result.error); } else { - queue.output(variables.data); + queue.output(result.output); } }); }); diff --git a/packages/client-api/src/providers/host/create-host-provider.ts b/packages/client-api/src/providers/host/create-host-provider.ts index 3573f993..77a1a21b 100644 --- a/packages/client-api/src/providers/host/create-host-provider.ts +++ b/packages/client-api/src/providers/host/create-host-provider.ts @@ -37,11 +37,11 @@ export async function createHostProvider( const mergedConfig = hostProviderConfigSchema.parse(config); return createBaseProvider(mergedConfig, async queue => { - return onProviderEmit(mergedConfig, ({ variables }) => { - if ('error' in variables) { - queue.error(variables.error); + return onProviderEmit(mergedConfig, ({ result }) => { + if ('error' in result) { + queue.error(result.error); } else { - queue.output(variables.data); + queue.output(result.output); } }); }); diff --git a/packages/client-api/src/providers/ip/create-ip-provider.ts b/packages/client-api/src/providers/ip/create-ip-provider.ts index efeae76d..496ddce2 100644 --- a/packages/client-api/src/providers/ip/create-ip-provider.ts +++ b/packages/client-api/src/providers/ip/create-ip-provider.ts @@ -36,11 +36,11 @@ export async function createIpProvider( const mergedConfig = ipProviderConfigSchema.parse(config); return createBaseProvider(mergedConfig, async queue => { - return onProviderEmit(mergedConfig, ({ variables }) => { - if ('error' in variables) { - queue.error(variables.error); + return onProviderEmit(mergedConfig, ({ result }) => { + if ('error' in result) { + queue.error(result.error); } else { - queue.output(variables.data); + queue.output(result.output); } }); }); diff --git a/packages/client-api/src/providers/komorebi/create-komorebi-provider.ts b/packages/client-api/src/providers/komorebi/create-komorebi-provider.ts index de2029a2..699b8ddf 100644 --- a/packages/client-api/src/providers/komorebi/create-komorebi-provider.ts +++ b/packages/client-api/src/providers/komorebi/create-komorebi-provider.ts @@ -180,11 +180,11 @@ export async function createKomorebiProvider( return onProviderEmit( mergedConfig, - async ({ variables }) => { - if ('error' in variables) { - queue.error(variables.error); + async ({ result }) => { + if ('error' in result) { + queue.error(result.error); } else { - const updatedState = await getUpdatedState(variables.data); + const updatedState = await getUpdatedState(result.output); queue.output(updatedState); } }, diff --git a/packages/client-api/src/providers/memory/create-memory-provider.ts b/packages/client-api/src/providers/memory/create-memory-provider.ts index 4ddf3dfd..102bd69c 100644 --- a/packages/client-api/src/providers/memory/create-memory-provider.ts +++ b/packages/client-api/src/providers/memory/create-memory-provider.ts @@ -38,11 +38,11 @@ export async function createMemoryProvider( const mergedConfig = memoryProviderConfigSchema.parse(config); return createBaseProvider(mergedConfig, async queue => { - return onProviderEmit(mergedConfig, ({ variables }) => { - if ('error' in variables) { - queue.error(variables.error); + return onProviderEmit(mergedConfig, ({ result }) => { + if ('error' in result) { + queue.error(result.error); } else { - queue.output(variables.data); + queue.output(result.output); } }); }); diff --git a/packages/client-api/src/providers/network/create-network-provider.ts b/packages/client-api/src/providers/network/create-network-provider.ts index 16bdae07..86a1d6b3 100644 --- a/packages/client-api/src/providers/network/create-network-provider.ts +++ b/packages/client-api/src/providers/network/create-network-provider.ts @@ -84,11 +84,11 @@ export async function createNetworkProvider( const mergedConfig = networkProviderConfigSchema.parse(config); return createBaseProvider(mergedConfig, async queue => { - return onProviderEmit(mergedConfig, ({ variables }) => { - if ('error' in variables) { - queue.error(variables.error); + return onProviderEmit(mergedConfig, ({ result }) => { + if ('error' in result) { + queue.error(result.error); } else { - queue.output(variables.data); + queue.output(result.output); } }); }); diff --git a/packages/client-api/src/providers/weather/create-weather-provider.ts b/packages/client-api/src/providers/weather/create-weather-provider.ts index afc558a7..176d8592 100644 --- a/packages/client-api/src/providers/weather/create-weather-provider.ts +++ b/packages/client-api/src/providers/weather/create-weather-provider.ts @@ -70,11 +70,11 @@ export async function createWeatherProvider( } return createBaseProvider(mergedConfig, async queue => { - return onProviderEmit(mergedConfig, ({ variables }) => { - if ('error' in variables) { - queue.error(variables.error); + return onProviderEmit(mergedConfig, ({ result }) => { + if ('error' in result) { + queue.error(result.error); } else { - queue.output(variables.data); + queue.output(result.output); } }); }); diff --git a/packages/desktop/src/providers/battery/provider.rs b/packages/desktop/src/providers/battery/provider.rs index 2af5cea5..5301d87c 100644 --- a/packages/desktop/src/providers/battery/provider.rs +++ b/packages/desktop/src/providers/battery/provider.rs @@ -11,12 +11,10 @@ use starship_battery::{ }; use tokio::{sync::mpsc, time}; -use super::{BatteryProviderConfig, BatteryVariables}; +use super::{BatteryOutput, BatteryProviderConfig}; use crate::providers::{ - provider::Provider, - provider_manager::SharedProviderState, - provider_ref::{ProviderOutput, VariablesResult}, - variables::ProviderVariables, + provider::Provider, provider_ref::ProviderResult, + variables::ProviderOutput, }; pub struct BatteryProvider { @@ -38,14 +36,14 @@ impl BatteryProvider { /// requires its own non-async function. pub fn get_variables( manager: &Manager, - ) -> anyhow::Result { + ) -> anyhow::Result { let battery = manager .batteries() .and_then(|mut batteries| batteries.nth(0).transpose()) .unwrap_or(None) .context("No battery found.")?; - Ok(ProviderVariables::Battery(BatteryVariables { + Ok(ProviderOutput::Battery(BatteryOutput { charge_percent: battery.state_of_charge().get::(), health_percent: battery.state_of_health().get::(), state: battery.state().to_string(), @@ -67,15 +65,14 @@ impl BatteryProvider { impl Provider for BatteryProvider { async fn on_start( self: Arc, - shared_state: SharedProviderState, - emit_output_tx: mpsc::Sender, + emit_result_tx: mpsc::Sender, ) { let mut interval = time::interval(Duration::from_millis(self.config.refresh_interval)); loop { interval.tick().await; - emit_output_tx + emit_result_tx .send(Self::get_variables(&self.battery_manager).into()) .await .unwrap(); diff --git a/packages/desktop/src/providers/battery/variables.rs b/packages/desktop/src/providers/battery/variables.rs index 0bc96e9c..dbb4cf57 100644 --- a/packages/desktop/src/providers/battery/variables.rs +++ b/packages/desktop/src/providers/battery/variables.rs @@ -2,7 +2,7 @@ use serde::Serialize; #[derive(Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] -pub struct BatteryVariables { +pub struct BatteryOutput { pub charge_percent: f32, pub health_percent: f32, pub state: String, diff --git a/packages/desktop/src/providers/cpu/provider.rs b/packages/desktop/src/providers/cpu/provider.rs index d4ab3d92..5c915111 100644 --- a/packages/desktop/src/providers/cpu/provider.rs +++ b/packages/desktop/src/providers/cpu/provider.rs @@ -4,23 +4,26 @@ use async_trait::async_trait; use sysinfo::System; use tokio::{ sync::{mpsc, Mutex}, - task::AbortHandle, time, }; -use super::{CpuProviderConfig, CpuVariables}; +use super::{CpuOutput, CpuProviderConfig}; use crate::providers::{ - provider::Provider, provider_manager::SharedProviderState, - provider_ref::VariablesResult, variables::ProviderVariables, + provider::Provider, provider_ref::ProviderResult, + variables::ProviderOutput, }; pub struct CpuProvider { config: CpuProviderConfig, + sysinfo: Arc>, } impl CpuProvider { - pub fn new(config: CpuProviderConfig) -> CpuProvider { - CpuProvider { config } + pub fn new( + config: CpuProviderConfig, + sysinfo: Arc>, + ) -> CpuProvider { + CpuProvider { config, sysinfo } } } @@ -28,8 +31,7 @@ impl CpuProvider { impl Provider for CpuProvider { async fn on_start( self: Arc, - shared_state: SharedProviderState, - emit_output_tx: mpsc::Sender, + emit_result_tx: mpsc::Sender, ) { let mut interval = time::interval(Duration::from_millis(self.config.refresh_interval)); @@ -37,10 +39,10 @@ impl Provider for CpuProvider { loop { interval.tick().await; - let mut sysinfo = shared_state.sysinfo.lock().await; + let mut sysinfo = self.sysinfo.lock().await; sysinfo.refresh_cpu(); - let res = Ok(ProviderVariables::Cpu(CpuVariables { + let res = Ok(ProviderOutput::Cpu(CpuOutput { usage: sysinfo.global_cpu_info().cpu_usage(), frequency: sysinfo.global_cpu_info().frequency(), logical_core_count: sysinfo.cpus().len(), @@ -50,7 +52,7 @@ impl Provider for CpuProvider { vendor: sysinfo.global_cpu_info().vendor_id().into(), })); - emit_output_tx.send(res.into()).await; + emit_result_tx.send(res.into()).await; } } } diff --git a/packages/desktop/src/providers/cpu/variables.rs b/packages/desktop/src/providers/cpu/variables.rs index f94d60b4..35a933eb 100644 --- a/packages/desktop/src/providers/cpu/variables.rs +++ b/packages/desktop/src/providers/cpu/variables.rs @@ -2,7 +2,7 @@ use serde::Serialize; #[derive(Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] -pub struct CpuVariables { +pub struct CpuOutput { pub frequency: u64, pub usage: f32, pub logical_core_count: usize, diff --git a/packages/desktop/src/providers/host/provider.rs b/packages/desktop/src/providers/host/provider.rs index b1f81c5f..485cd5bb 100644 --- a/packages/desktop/src/providers/host/provider.rs +++ b/packages/desktop/src/providers/host/provider.rs @@ -4,9 +4,9 @@ use async_trait::async_trait; use sysinfo::System; use tokio::{sync::Mutex, task::AbortHandle}; -use super::{HostProviderConfig, HostVariables}; +use super::{HostProviderConfig, HostOutput}; use crate::providers::{ - provider::IntervalProvider, variables::ProviderVariables, + provider::IntervalProvider, variables::ProviderOutput, }; pub struct HostProvider { @@ -52,8 +52,8 @@ impl IntervalProvider for HostProvider { async fn get_refreshed_variables( _: &HostProviderConfig, __: &Mutex, - ) -> anyhow::Result { - Ok(ProviderVariables::Host(HostVariables { + ) -> anyhow::Result { + Ok(ProviderOutput::Host(HostOutput { hostname: System::host_name(), os_name: System::name(), os_version: System::os_version(), diff --git a/packages/desktop/src/providers/host/variables.rs b/packages/desktop/src/providers/host/variables.rs index 5b1b7137..15a7629b 100644 --- a/packages/desktop/src/providers/host/variables.rs +++ b/packages/desktop/src/providers/host/variables.rs @@ -2,7 +2,7 @@ use serde::Serialize; #[derive(Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] -pub struct HostVariables { +pub struct HostOutput { pub hostname: Option, pub os_name: Option, pub os_version: Option, diff --git a/packages/desktop/src/providers/ip/provider.rs b/packages/desktop/src/providers/ip/provider.rs index 0fa936be..23acfeed 100644 --- a/packages/desktop/src/providers/ip/provider.rs +++ b/packages/desktop/src/providers/ip/provider.rs @@ -5,9 +5,9 @@ use async_trait::async_trait; use reqwest::Client; use tokio::task::AbortHandle; -use super::{ipinfo_res::IpinfoRes, IpProviderConfig, IpVariables}; +use super::{ipinfo_res::IpinfoRes, IpProviderConfig, IpOutput}; use crate::providers::{ - provider::IntervalProvider, variables::ProviderVariables, + provider::IntervalProvider, variables::ProviderOutput, }; pub struct IpProvider { @@ -50,7 +50,7 @@ impl IntervalProvider for IpProvider { async fn get_refreshed_variables( _: &IpProviderConfig, http_client: &Client, - ) -> anyhow::Result { + ) -> anyhow::Result { let res = http_client .get("https://ipinfo.io/json") .send() @@ -60,7 +60,7 @@ impl IntervalProvider for IpProvider { let mut loc_parts = res.loc.split(','); - Ok(ProviderVariables::Ip(IpVariables { + Ok(ProviderOutput::Ip(IpOutput { address: res.ip, approx_city: res.city, approx_country: res.country, diff --git a/packages/desktop/src/providers/ip/variables.rs b/packages/desktop/src/providers/ip/variables.rs index 24e1e6e8..198be0b2 100644 --- a/packages/desktop/src/providers/ip/variables.rs +++ b/packages/desktop/src/providers/ip/variables.rs @@ -2,7 +2,7 @@ use serde::Serialize; #[derive(Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] -pub struct IpVariables { +pub struct IpOutput { pub address: String, pub approx_city: String, pub approx_country: String, diff --git a/packages/desktop/src/providers/komorebi/provider.rs b/packages/desktop/src/providers/komorebi/provider.rs index 11784e6f..80569f97 100644 --- a/packages/desktop/src/providers/komorebi/provider.rs +++ b/packages/desktop/src/providers/komorebi/provider.rs @@ -14,10 +14,8 @@ use super::{ KomorebiProviderConfig, KomorebiWindow, KomorebiWorkspace, }; use crate::providers::{ - komorebi::KomorebiVariables, - provider::Provider, - provider_ref::{ProviderOutput, VariablesResult}, - variables::ProviderVariables, + komorebi::KomorebiOutput, provider::Provider, + provider_ref::ProviderResult, variables::ProviderOutput, }; const SOCKET_NAME: &str = "zebar.sock"; @@ -35,9 +33,7 @@ impl KomorebiProvider { } } - fn transform_response( - state: komorebi_client::State, - ) -> KomorebiVariables { + fn transform_response(state: komorebi_client::State) -> KomorebiOutput { let all_monitors = state .monitors .elements() @@ -45,7 +41,7 @@ impl KomorebiProvider { .map(Self::transform_monitor) .collect(); - KomorebiVariables { + KomorebiOutput { all_monitors, focused_monitor_index: state.monitors.focused_idx(), } @@ -123,7 +119,7 @@ impl Provider for KomorebiProvider { async fn on_start( &mut self, config_hash: &str, - emit_output_tx: Sender, + emit_result_tx: Sender, ) { let config_hash = config_hash.to_string(); @@ -142,24 +138,22 @@ impl Provider for KomorebiProvider { serde_json::from_str::(&line) { // Transform and emit the incoming Komorebi state. - _ = emit_output_tx + _ = emit_result_tx .send(ProviderOutput { config_hash: config_hash.clone(), - variables: VariablesResult::Data( - ProviderVariables::Komorebi(Self::transform_response( - notification.state, - )), - ), + variables: OutputResult::Data(ProviderOutput::Komorebi( + Self::transform_response(notification.state), + )), }) .await; } } } Err(error) => { - _ = emit_output_tx + _ = emit_result_tx .send(ProviderOutput { config_hash: config_hash.to_string(), - variables: VariablesResult::Error(error.to_string()), + variables: OutputResult::Error(error.to_string()), }) .await; } diff --git a/packages/desktop/src/providers/komorebi/variables.rs b/packages/desktop/src/providers/komorebi/variables.rs index 4780141f..d79fe9f0 100644 --- a/packages/desktop/src/providers/komorebi/variables.rs +++ b/packages/desktop/src/providers/komorebi/variables.rs @@ -3,7 +3,7 @@ use serde::Serialize; #[derive(Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] -pub struct KomorebiVariables { +pub struct KomorebiOutput { pub all_monitors: Vec, pub focused_monitor_index: usize, } diff --git a/packages/desktop/src/providers/memory/provider.rs b/packages/desktop/src/providers/memory/provider.rs index 75dda699..8002a73a 100644 --- a/packages/desktop/src/providers/memory/provider.rs +++ b/packages/desktop/src/providers/memory/provider.rs @@ -4,9 +4,9 @@ use async_trait::async_trait; use sysinfo::System; use tokio::{sync::Mutex, task::AbortHandle}; -use super::{MemoryProviderConfig, MemoryVariables}; +use super::{MemoryProviderConfig, MemoryOutput}; use crate::providers::{ - provider::IntervalProvider, variables::ProviderVariables, + provider::IntervalProvider, variables::ProviderOutput, }; pub struct MemoryProvider { @@ -52,7 +52,7 @@ impl IntervalProvider for MemoryProvider { async fn get_refreshed_variables( _: &MemoryProviderConfig, sysinfo: &Mutex, - ) -> anyhow::Result { + ) -> anyhow::Result { let mut sysinfo = sysinfo.lock().await; sysinfo.refresh_memory(); @@ -60,7 +60,7 @@ impl IntervalProvider for MemoryProvider { / sysinfo.total_memory() as f32) * 100.0; - Ok(ProviderVariables::Memory(MemoryVariables { + Ok(ProviderOutput::Memory(MemoryOutput { usage, free_memory: sysinfo.free_memory(), used_memory: sysinfo.used_memory(), diff --git a/packages/desktop/src/providers/memory/variables.rs b/packages/desktop/src/providers/memory/variables.rs index f940b0eb..21b121ee 100644 --- a/packages/desktop/src/providers/memory/variables.rs +++ b/packages/desktop/src/providers/memory/variables.rs @@ -2,7 +2,7 @@ use serde::Serialize; #[derive(Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] -pub struct MemoryVariables { +pub struct MemoryOutput { pub usage: f32, pub free_memory: u64, pub used_memory: u64, diff --git a/packages/desktop/src/providers/network/provider.rs b/packages/desktop/src/providers/network/provider.rs index 480cd20f..c3b92232 100644 --- a/packages/desktop/src/providers/network/provider.rs +++ b/packages/desktop/src/providers/network/provider.rs @@ -8,10 +8,10 @@ use tokio::{sync::Mutex, task::AbortHandle}; use super::{ wifi_hotspot::{default_gateway_wifi, WifiHotstop}, InterfaceType, NetworkGateway, NetworkInterface, NetworkProviderConfig, - NetworkTraffic, NetworkVariables, + NetworkTraffic, NetworkOutput, }; use crate::providers::{ - provider::IntervalProvider, variables::ProviderVariables, + provider::IntervalProvider, variables::ProviderOutput, }; pub struct NetworkProvider { @@ -110,7 +110,7 @@ impl IntervalProvider for NetworkProvider { async fn get_refreshed_variables( config: &NetworkProviderConfig, netinfo: &Mutex, - ) -> anyhow::Result { + ) -> anyhow::Result { let mut netinfo = netinfo.lock().await; netinfo.refresh(); @@ -118,7 +118,7 @@ impl IntervalProvider for NetworkProvider { let default_interface = netdev::get_default_interface().ok(); - let variables = NetworkVariables { + let variables = NetworkOutput { default_interface: default_interface .as_ref() .map(Self::transform_interface), @@ -145,7 +145,7 @@ impl IntervalProvider for NetworkProvider { }, }; - Ok(ProviderVariables::Network(variables)) + Ok(ProviderOutput::Network(variables)) } } diff --git a/packages/desktop/src/providers/network/variables.rs b/packages/desktop/src/providers/network/variables.rs index 8e287fe1..eeb013f3 100644 --- a/packages/desktop/src/providers/network/variables.rs +++ b/packages/desktop/src/providers/network/variables.rs @@ -3,7 +3,7 @@ use serde::Serialize; #[derive(Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] -pub struct NetworkVariables { +pub struct NetworkOutput { pub default_interface: Option, pub default_gateway: Option, pub interfaces: Vec, diff --git a/packages/desktop/src/providers/provider.rs b/packages/desktop/src/providers/provider.rs index 6ff8122e..e8185599 100644 --- a/packages/desktop/src/providers/provider.rs +++ b/packages/desktop/src/providers/provider.rs @@ -3,17 +3,14 @@ use std::sync::Arc; use async_trait::async_trait; use tokio::sync::mpsc::Sender; -use super::{ - provider_manager::SharedProviderState, provider_ref::VariablesResult, -}; +use super::provider_ref::ProviderResult; #[async_trait] pub trait Provider: Send + Sync { /// Callback for when the provider is started. async fn on_start( self: Arc, - shared_state: SharedProviderState, - emit_output_tx: Sender, + emit_result_tx: Sender, ); /// Callback for when the provider is stopped. diff --git a/packages/desktop/src/providers/provider_manager.rs b/packages/desktop/src/providers/provider_manager.rs index 20db3b67..3b6b6960 100644 --- a/packages/desktop/src/providers/provider_manager.rs +++ b/packages/desktop/src/providers/provider_manager.rs @@ -18,6 +18,7 @@ pub struct SharedProviderState { pub struct ProviderManager { app_handle: AppHandle, providers: Arc>>, + // provider_cache: Arc>>, shared_state: SharedProviderState, } @@ -26,6 +27,7 @@ impl ProviderManager { Self { app_handle: app_handle.clone(), providers: Arc::new(Mutex::new(HashMap::new())), + // provider_cache: Arc::new(Mutex::new(HashMap::new())), shared_state: SharedProviderState { sysinfo: Arc::new(Mutex::new(System::new_all())), netinfo: Arc::new(Mutex::new(Networks::new_with_refreshed_list())), diff --git a/packages/desktop/src/providers/provider_ref.rs b/packages/desktop/src/providers/provider_ref.rs index ea44197b..1a70dc96 100644 --- a/packages/desktop/src/providers/provider_ref.rs +++ b/packages/desktop/src/providers/provider_ref.rs @@ -1,10 +1,8 @@ -use std::{ - sync::Arc, - time::{Duration, Instant}, -}; +use std::{sync::Arc, time::Instant}; use anyhow::bail; use serde::Serialize; +use serde_json::json; use tauri::{AppHandle, Emitter}; use tokio::{sync::mpsc, task}; use tracing::{info, warn}; @@ -19,7 +17,7 @@ use super::{ // network::NetworkProvider, provider::Provider, provider_manager::SharedProviderState, - variables::ProviderVariables, + variables::ProviderOutput, // weather::WeatherProvider, }; @@ -28,7 +26,7 @@ pub struct ProviderRef { pub config_hash: String, pub cache: Option, provider: Arc, - emit_output_tx: mpsc::Sender, + emit_result_tx: mpsc::Sender, } /// Cache for provider output. @@ -38,31 +36,23 @@ pub struct ProviderCache { pub output: Box, } -/// Output emitted to frontend clients. -#[derive(Serialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct ProviderOutput { - pub config_hash: String, - pub variables: VariablesResult, -} - -/// Provider variable output emitted to frontend clients. +/// Provider output/error emitted to frontend clients. /// -/// This is used instead of a normal `Result` type to serialize it in a -/// nicer way. +/// This is used instead of a normal `Result` type in order to serialize it +/// in a nicer way. #[derive(Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] -pub enum VariablesResult { - Data(ProviderVariables), +pub enum ProviderResult { + Output(ProviderOutput), Error(String), } -/// Implements conversion from an `anyhow::Result`. -impl From> for VariablesResult { - fn from(result: anyhow::Result) -> Self { +/// Implements conversion from `anyhow::Result`. +impl From> for ProviderResult { + fn from(result: anyhow::Result) -> Self { match result { - Ok(data) => VariablesResult::Data(data), - Err(err) => VariablesResult::Error(err.to_string()), + Ok(output) => ProviderResult::Output(output), + Err(err) => ProviderResult::Error(err.to_string()), } } } @@ -77,22 +67,22 @@ impl ProviderRef { ) -> anyhow::Result { let provider = Self::create_provider(config, shared_state)?; - let (emit_output_tx, mut emit_output_rx) = - mpsc::channel::(1); + let (emit_result_tx, mut emit_result_rx) = + mpsc::channel::(1); let config_hash_clone = config_hash.clone(); let app_handle = app_handle.clone(); task::spawn(async move { - while let Some(output) = emit_output_rx.recv().await { + while let Some(output) = emit_result_rx.recv().await { info!("Emitting for provider: {}", config_hash_clone); let output = Box::new(output); - let xx = ProviderOutput { - config_hash: config_hash_clone.clone(), - variables: *output.clone(), - }; + let payload = json!({ + "config_hash": config_hash_clone.clone(), + "result": *output.clone(), + }); - if let Err(err) = app_handle.emit("provider-emit", xx) { + if let Err(err) = app_handle.emit("provider-emit", payload) { warn!("Error emitting provider output: {:?}", err); } @@ -111,19 +101,16 @@ impl ProviderRef { info!("Starting provider: {}", config_hash); let provider_clone = provider.clone(); - let shared_state = shared_state.clone(); - let emit_output_tx_clone = emit_output_tx.clone(); + let emit_result_tx_clone = emit_result_tx.clone(); task::spawn(async move { - provider_clone - .on_start(shared_state.clone(), emit_output_tx_clone) - .await; + provider_clone.on_start(emit_result_tx_clone).await; }); Ok(Self { config_hash, cache: None, provider, - emit_output_tx, + emit_result_tx, }) } @@ -135,7 +122,9 @@ impl ProviderRef { ProviderConfig::Battery(config) => { Arc::new(BatteryProvider::new(config)?) } - ProviderConfig::Cpu(config) => Arc::new(CpuProvider::new(config)), + ProviderConfig::Cpu(config) => { + Arc::new(CpuProvider::new(config, shared_state.sysinfo.clone())) + } // ProviderConfig::Host(config) => { // Box::new(HostProvider::new(config, // shared_state.sysinfo.clone())) } @@ -175,7 +164,7 @@ impl ProviderRef { /// minimum refresh interval, send the previous output. pub async fn refresh(&mut self) -> anyhow::Result<()> { // if let Some(cache) = self.cache { - // self.emit_output_tx.send(*cache.output.clone()).await?; + // self.emit_result_tx.send(*cache.output.clone()).await?; // } Ok(()) diff --git a/packages/desktop/src/providers/variables.rs b/packages/desktop/src/providers/variables.rs index 2f9c9cdf..e123ece6 100644 --- a/packages/desktop/src/providers/variables.rs +++ b/packages/desktop/src/providers/variables.rs @@ -1,25 +1,25 @@ use serde::Serialize; // #[cfg(windows)] -// use super::komorebi::KomorebiVariables; +// use super::komorebi::KomorebiOutput; use super::{ - battery::BatteryVariables, - cpu::CpuVariables, - // host::HostVariables, - // ip::IpVariables, memory::MemoryVariables, network::NetworkVariables, - // weather::WeatherVariables, + battery::BatteryOutput, + cpu::CpuOutput, + // host::HostOutput, + // ip::IpOutput, memory::MemoryOutput, network::NetworkOutput, + // weather::WeatherOutput, }; #[derive(Serialize, Debug, Clone)] #[serde(untagged)] -pub enum ProviderVariables { - Battery(BatteryVariables), - Cpu(CpuVariables), - // Host(HostVariables), - // Ip(IpVariables), +pub enum ProviderOutput { + Battery(BatteryOutput), + Cpu(CpuOutput), + // Host(HostOutput), + // Ip(IpOutput), // #[cfg(windows)] - // Komorebi(KomorebiVariables), - // Memory(MemoryVariables), - // Network(NetworkVariables), - // Weather(WeatherVariables), + // Komorebi(KomorebiOutput), + // Memory(MemoryOutput), + // Network(NetworkOutput), + // Weather(WeatherOutput), } diff --git a/packages/desktop/src/providers/weather/provider.rs b/packages/desktop/src/providers/weather/provider.rs index c5e7e553..7191775e 100644 --- a/packages/desktop/src/providers/weather/provider.rs +++ b/packages/desktop/src/providers/weather/provider.rs @@ -6,10 +6,10 @@ use tokio::task::AbortHandle; use super::{ open_meteo_res::OpenMeteoRes, WeatherProviderConfig, WeatherStatus, - WeatherVariables, + WeatherOutput, }; use crate::providers::{ - provider::IntervalProvider, variables::ProviderVariables, + provider::IntervalProvider, variables::ProviderOutput, }; pub struct WeatherProvider { @@ -94,7 +94,7 @@ impl IntervalProvider for WeatherProvider { async fn get_refreshed_variables( config: &WeatherProviderConfig, http_client: &Client, - ) -> anyhow::Result { + ) -> anyhow::Result { let res = http_client .get("https://api.open-meteo.com/v1/forecast") .query(&[ @@ -113,7 +113,7 @@ impl IntervalProvider for WeatherProvider { let current_weather = res.current_weather; let is_daytime = current_weather.is_day == 1; - Ok(ProviderVariables::Weather(WeatherVariables { + Ok(ProviderOutput::Weather(WeatherOutput { is_daytime, status: Self::get_weather_status( current_weather.weather_code, diff --git a/packages/desktop/src/providers/weather/variables.rs b/packages/desktop/src/providers/weather/variables.rs index 94f5df50..8f558e38 100644 --- a/packages/desktop/src/providers/weather/variables.rs +++ b/packages/desktop/src/providers/weather/variables.rs @@ -2,7 +2,7 @@ use serde::Serialize; #[derive(Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] -pub struct WeatherVariables { +pub struct WeatherOutput { pub is_daytime: bool, pub status: WeatherStatus, pub celsius_temp: f32, From 18160d911c275a6c1c28e822612b7a9fd3837401 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Fri, 6 Sep 2024 02:33:51 +0800 Subject: [PATCH 083/138] feat: change all providers to implement new `Provider` trait --- packages/desktop/src/providers/config.rs | 27 ++-- packages/desktop/src/providers/cpu/config.rs | 4 - packages/desktop/src/providers/host/config.rs | 4 - .../desktop/src/providers/host/provider.rs | 67 ++++----- packages/desktop/src/providers/ip/config.rs | 4 - packages/desktop/src/providers/ip/provider.rs | 63 ++++---- .../src/providers/komorebi/provider.rs | 119 +++++++++------ .../desktop/src/providers/memory/config.rs | 4 - .../desktop/src/providers/memory/provider.rs | 67 ++++----- packages/desktop/src/providers/mod.rs | 14 +- .../desktop/src/providers/network/config.rs | 4 - .../desktop/src/providers/network/provider.rs | 141 ++++++++---------- .../desktop/src/providers/provider_ref.rs | 54 ++++--- packages/desktop/src/providers/variables.rs | 25 ++-- .../desktop/src/providers/weather/config.rs | 4 - .../desktop/src/providers/weather/provider.rs | 126 ++++++++-------- 16 files changed, 347 insertions(+), 380 deletions(-) diff --git a/packages/desktop/src/providers/config.rs b/packages/desktop/src/providers/config.rs index e4b4c83e..40da9c9a 100644 --- a/packages/desktop/src/providers/config.rs +++ b/packages/desktop/src/providers/config.rs @@ -1,13 +1,12 @@ use serde::Deserialize; -// #[cfg(windows)] -// use super::komorebi::KomorebiProviderConfig; +#[cfg(windows)] +use super::komorebi::KomorebiProviderConfig; use super::{ - battery::BatteryProviderConfig, - cpu::CpuProviderConfig, - // host::HostProviderConfig, ip::IpProviderConfig, - // memory::MemoryProviderConfig, network::NetworkProviderConfig, - // weather::WeatherProviderConfig, + battery::BatteryProviderConfig, cpu::CpuProviderConfig, + host::HostProviderConfig, ip::IpProviderConfig, + memory::MemoryProviderConfig, network::NetworkProviderConfig, + weather::WeatherProviderConfig, }; #[derive(Deserialize, Debug)] @@ -15,11 +14,11 @@ use super::{ pub enum ProviderConfig { Battery(BatteryProviderConfig), Cpu(CpuProviderConfig), - // Host(HostProviderConfig), - // Ip(IpProviderConfig), - // #[cfg(windows)] - // Komorebi(KomorebiProviderConfig), - // Memory(MemoryProviderConfig), - // Network(NetworkProviderConfig), - // Weather(WeatherProviderConfig), + Host(HostProviderConfig), + Ip(IpProviderConfig), + #[cfg(windows)] + Komorebi(KomorebiProviderConfig), + Memory(MemoryProviderConfig), + Network(NetworkProviderConfig), + Weather(WeatherProviderConfig), } diff --git a/packages/desktop/src/providers/cpu/config.rs b/packages/desktop/src/providers/cpu/config.rs index a0d0ccd3..e186ce4b 100644 --- a/packages/desktop/src/providers/cpu/config.rs +++ b/packages/desktop/src/providers/cpu/config.rs @@ -1,11 +1,7 @@ use serde::Deserialize; -// use crate::impl_interval_config; - #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct CpuProviderConfig { pub refresh_interval: u64, } - -// impl_interval_config!(CpuProviderConfig); diff --git a/packages/desktop/src/providers/host/config.rs b/packages/desktop/src/providers/host/config.rs index 96e79ee9..f56a1431 100644 --- a/packages/desktop/src/providers/host/config.rs +++ b/packages/desktop/src/providers/host/config.rs @@ -1,11 +1,7 @@ use serde::Deserialize; -use crate::impl_interval_config; - #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct HostProviderConfig { pub refresh_interval: u64, } - -impl_interval_config!(HostProviderConfig); diff --git a/packages/desktop/src/providers/host/provider.rs b/packages/desktop/src/providers/host/provider.rs index 485cd5bb..1e4623c6 100644 --- a/packages/desktop/src/providers/host/provider.rs +++ b/packages/desktop/src/providers/host/provider.rs @@ -1,17 +1,20 @@ -use std::sync::Arc; +use std::{sync::Arc, time::Duration}; use async_trait::async_trait; use sysinfo::System; -use tokio::{sync::Mutex, task::AbortHandle}; +use tokio::{ + sync::{mpsc, Mutex}, + time, +}; -use super::{HostProviderConfig, HostOutput}; +use super::{HostOutput, HostProviderConfig}; use crate::providers::{ - provider::IntervalProvider, variables::ProviderOutput, + provider::Provider, provider_ref::ProviderResult, + variables::ProviderOutput, }; pub struct HostProvider { - pub config: Arc, - abort_handle: Option, + config: HostProviderConfig, sysinfo: Arc>, } @@ -20,39 +23,10 @@ impl HostProvider { config: HostProviderConfig, sysinfo: Arc>, ) -> HostProvider { - HostProvider { - config: Arc::new(config), - abort_handle: None, - sysinfo, - } - } -} - -#[async_trait] -impl IntervalProvider for HostProvider { - type Config = HostProviderConfig; - type State = Mutex; - - fn config(&self) -> Arc { - self.config.clone() - } - - fn state(&self) -> Arc> { - self.sysinfo.clone() + HostProvider { config, sysinfo } } - fn abort_handle(&self) -> &Option { - &self.abort_handle - } - - fn set_abort_handle(&mut self, abort_handle: AbortHandle) { - self.abort_handle = Some(abort_handle) - } - - async fn get_refreshed_variables( - _: &HostProviderConfig, - __: &Mutex, - ) -> anyhow::Result { + async fn interval_output() -> anyhow::Result { Ok(ProviderOutput::Host(HostOutput { hostname: System::host_name(), os_name: System::name(), @@ -63,3 +37,22 @@ impl IntervalProvider for HostProvider { })) } } + +#[async_trait] +impl Provider for HostProvider { + async fn on_start( + self: Arc, + emit_result_tx: mpsc::Sender, + ) { + let mut interval = + time::interval(Duration::from_millis(self.config.refresh_interval)); + + loop { + interval.tick().await; + + emit_result_tx + .send(Self::interval_output().await.into()) + .await; + } + } +} diff --git a/packages/desktop/src/providers/ip/config.rs b/packages/desktop/src/providers/ip/config.rs index ffe6ea14..c2d8a597 100644 --- a/packages/desktop/src/providers/ip/config.rs +++ b/packages/desktop/src/providers/ip/config.rs @@ -1,11 +1,7 @@ use serde::Deserialize; -use crate::impl_interval_config; - #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct IpProviderConfig { pub refresh_interval: u64, } - -impl_interval_config!(IpProviderConfig); diff --git a/packages/desktop/src/providers/ip/provider.rs b/packages/desktop/src/providers/ip/provider.rs index 23acfeed..8be390d5 100644 --- a/packages/desktop/src/providers/ip/provider.rs +++ b/packages/desktop/src/providers/ip/provider.rs @@ -1,54 +1,30 @@ -use std::sync::Arc; +use std::{sync::Arc, time::Duration}; use anyhow::Context; use async_trait::async_trait; use reqwest::Client; -use tokio::task::AbortHandle; +use tokio::{sync::mpsc, time}; -use super::{ipinfo_res::IpinfoRes, IpProviderConfig, IpOutput}; +use super::{ipinfo_res::IpinfoRes, IpOutput, IpProviderConfig}; use crate::providers::{ - provider::IntervalProvider, variables::ProviderOutput, + provider::Provider, provider_ref::ProviderResult, + variables::ProviderOutput, }; pub struct IpProvider { - pub config: Arc, - abort_handle: Option, - http_client: Arc, + config: IpProviderConfig, + http_client: Client, } impl IpProvider { pub fn new(config: IpProviderConfig) -> IpProvider { IpProvider { - config: Arc::new(config), - abort_handle: None, - http_client: Arc::new(Client::new()), + config, + http_client: Client::new(), } } -} - -#[async_trait] -impl IntervalProvider for IpProvider { - type Config = IpProviderConfig; - type State = Client; - - fn config(&self) -> Arc { - self.config.clone() - } - - fn state(&self) -> Arc { - self.http_client.clone() - } - - fn abort_handle(&self) -> &Option { - &self.abort_handle - } - fn set_abort_handle(&mut self, abort_handle: AbortHandle) { - self.abort_handle = Some(abort_handle) - } - - async fn get_refreshed_variables( - _: &IpProviderConfig, + async fn interval_output( http_client: &Client, ) -> anyhow::Result { let res = http_client @@ -75,3 +51,22 @@ impl IntervalProvider for IpProvider { })) } } + +#[async_trait] +impl Provider for IpProvider { + async fn on_start( + self: Arc, + emit_result_tx: mpsc::Sender, + ) { + let mut interval = + time::interval(Duration::from_millis(self.config.refresh_interval)); + + loop { + interval.tick().await; + + emit_result_tx + .send(Self::interval_output(&self.http_client).await.into()) + .await; + } + } +} diff --git a/packages/desktop/src/providers/komorebi/provider.rs b/packages/desktop/src/providers/komorebi/provider.rs index 80569f97..17fee2b8 100644 --- a/packages/desktop/src/providers/komorebi/provider.rs +++ b/packages/desktop/src/providers/komorebi/provider.rs @@ -1,12 +1,15 @@ use std::{ - io::{BufRead, BufReader}, + io::{BufReader, Read}, sync::Arc, time::Duration, }; +use anyhow::Context; use async_trait::async_trait; -use komorebi_client::{Container, Monitor, Window, Workspace}; -use tokio::{sync::mpsc::Sender, task::AbortHandle}; +use komorebi_client::{ + Container, Monitor, SocketMessage, Window, Workspace, +}; +use tokio::{sync::mpsc::Sender, time}; use tracing::debug; use super::{ @@ -21,16 +24,74 @@ use crate::providers::{ const SOCKET_NAME: &str = "zebar.sock"; pub struct KomorebiProvider { - pub config: Arc, - abort_handle: Option, + config: KomorebiProviderConfig, } impl KomorebiProvider { pub fn new(config: KomorebiProviderConfig) -> KomorebiProvider { - KomorebiProvider { - config: Arc::new(config), - abort_handle: None, + KomorebiProvider { config } + } + + async fn create_socket( + self: Arc, + emit_result_tx: Sender, + ) -> anyhow::Result<()> { + let socket = komorebi_client::subscribe(SOCKET_NAME) + .context("Failed to initialize Komorebi socket.")?; + + debug!("Connected to Komorebi socket."); + + for incoming in socket.incoming() { + debug!("Incoming Komorebi socket message."); + + match incoming { + Ok(stream) => { + let mut buffer = Vec::new(); + let mut reader = BufReader::new(stream); + + // Shutdown has been sent. + if matches!(reader.read_to_end(&mut buffer), Ok(0)) { + debug!("Komorebi shutdown."); + + // Attempt to reconnect to Komorebi. + while komorebi_client::send_message( + &SocketMessage::AddSubscriberSocket(SOCKET_NAME.to_string()), + ) + .is_err() + { + debug!("Attempting to reconnect to Komorebi."); + time::sleep(Duration::from_secs(15)).await; + } + } + + // Transform and emit the incoming Komorebi state. + if let Ok(notification) = + serde_json::from_str::( + &String::from_utf8(buffer).unwrap(), + ) + { + emit_result_tx + .send( + Ok(ProviderOutput::Komorebi(Self::transform_response( + notification.state, + ))) + .into(), + ) + .await; + } + } + Err(_) => { + emit_result_tx + .send( + Err(anyhow::anyhow!("Failed to read Komorebi stream.")) + .into(), + ) + .await; + } + } } + + Ok(()) } fn transform_response(state: komorebi_client::State) -> KomorebiOutput { @@ -117,47 +178,11 @@ impl KomorebiProvider { #[async_trait] impl Provider for KomorebiProvider { async fn on_start( - &mut self, - config_hash: &str, + self: Arc, emit_result_tx: Sender, ) { - let config_hash = config_hash.to_string(); - - let socket = komorebi_client::subscribe(SOCKET_NAME).unwrap(); - debug!("Connected to Komorebi socket."); - - for incoming in socket.incoming() { - debug!("Incoming Komorebi socket message."); - - match incoming { - Ok(data) => { - let reader = BufReader::new(data.try_clone().unwrap()); - - for line in reader.lines().flatten() { - if let Ok(notification) = - serde_json::from_str::(&line) - { - // Transform and emit the incoming Komorebi state. - _ = emit_result_tx - .send(ProviderOutput { - config_hash: config_hash.clone(), - variables: OutputResult::Data(ProviderOutput::Komorebi( - Self::transform_response(notification.state), - )), - }) - .await; - } - } - } - Err(error) => { - _ = emit_result_tx - .send(ProviderOutput { - config_hash: config_hash.to_string(), - variables: OutputResult::Error(error.to_string()), - }) - .await; - } - } + if let Err(err) = self.create_socket(emit_result_tx.clone()).await { + emit_result_tx.send(Err(err).into()).await; } } } diff --git a/packages/desktop/src/providers/memory/config.rs b/packages/desktop/src/providers/memory/config.rs index b71b8649..2fa8b11e 100644 --- a/packages/desktop/src/providers/memory/config.rs +++ b/packages/desktop/src/providers/memory/config.rs @@ -1,11 +1,7 @@ use serde::Deserialize; -use crate::impl_interval_config; - #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct MemoryProviderConfig { pub refresh_interval: u64, } - -impl_interval_config!(MemoryProviderConfig); diff --git a/packages/desktop/src/providers/memory/provider.rs b/packages/desktop/src/providers/memory/provider.rs index 8002a73a..3344406a 100644 --- a/packages/desktop/src/providers/memory/provider.rs +++ b/packages/desktop/src/providers/memory/provider.rs @@ -1,17 +1,20 @@ -use std::sync::Arc; +use std::{sync::Arc, time::Duration}; use async_trait::async_trait; use sysinfo::System; -use tokio::{sync::Mutex, task::AbortHandle}; +use tokio::{ + sync::{mpsc, Mutex}, + time, +}; -use super::{MemoryProviderConfig, MemoryOutput}; +use super::{MemoryOutput, MemoryProviderConfig}; use crate::providers::{ - provider::IntervalProvider, variables::ProviderOutput, + provider::Provider, provider_ref::ProviderResult, + variables::ProviderOutput, }; pub struct MemoryProvider { - pub config: Arc, - abort_handle: Option, + config: MemoryProviderConfig, sysinfo: Arc>, } @@ -20,38 +23,11 @@ impl MemoryProvider { config: MemoryProviderConfig, sysinfo: Arc>, ) -> MemoryProvider { - MemoryProvider { - config: Arc::new(config), - abort_handle: None, - sysinfo, - } - } -} - -#[async_trait] -impl IntervalProvider for MemoryProvider { - type Config = MemoryProviderConfig; - type State = Mutex; - - fn config(&self) -> Arc { - self.config.clone() - } - - fn state(&self) -> Arc> { - self.sysinfo.clone() - } - - fn abort_handle(&self) -> &Option { - &self.abort_handle + MemoryProvider { config, sysinfo } } - fn set_abort_handle(&mut self, abort_handle: AbortHandle) { - self.abort_handle = Some(abort_handle) - } - - async fn get_refreshed_variables( - _: &MemoryProviderConfig, - sysinfo: &Mutex, + async fn interval_output( + sysinfo: Arc>, ) -> anyhow::Result { let mut sysinfo = sysinfo.lock().await; sysinfo.refresh_memory(); @@ -71,3 +47,22 @@ impl IntervalProvider for MemoryProvider { })) } } + +#[async_trait] +impl Provider for MemoryProvider { + async fn on_start( + self: Arc, + emit_result_tx: mpsc::Sender, + ) { + let mut interval = + time::interval(Duration::from_millis(self.config.refresh_interval)); + + loop { + interval.tick().await; + + emit_result_tx + .send(Self::interval_output().await.into()) + .await; + } + } +} diff --git a/packages/desktop/src/providers/mod.rs b/packages/desktop/src/providers/mod.rs index 75f79edc..b902a359 100644 --- a/packages/desktop/src/providers/mod.rs +++ b/packages/desktop/src/providers/mod.rs @@ -1,14 +1,14 @@ pub mod battery; pub mod config; pub mod cpu; -// pub mod host; -// pub mod ip; -// #[cfg(windows)] -// pub mod komorebi; -// pub mod memory; -// pub mod network; +pub mod host; +pub mod ip; +#[cfg(windows)] +pub mod komorebi; +pub mod memory; +pub mod network; pub mod provider; pub mod provider_manager; pub mod provider_ref; pub mod variables; -// pub mod weather; +pub mod weather; diff --git a/packages/desktop/src/providers/network/config.rs b/packages/desktop/src/providers/network/config.rs index 873f08e8..b1eda959 100644 --- a/packages/desktop/src/providers/network/config.rs +++ b/packages/desktop/src/providers/network/config.rs @@ -1,11 +1,7 @@ use serde::Deserialize; -use crate::impl_interval_config; - #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct NetworkProviderConfig { pub refresh_interval: u64, } - -impl_interval_config!(NetworkProviderConfig); diff --git a/packages/desktop/src/providers/network/provider.rs b/packages/desktop/src/providers/network/provider.rs index c3b92232..6fe87c95 100644 --- a/packages/desktop/src/providers/network/provider.rs +++ b/packages/desktop/src/providers/network/provider.rs @@ -1,23 +1,25 @@ -use std::sync::Arc; +use std::{sync::Arc, time::Duration}; use async_trait::async_trait; use netdev::interface::get_interfaces; use sysinfo::Networks; -use tokio::{sync::Mutex, task::AbortHandle}; +use tokio::{ + sync::{mpsc, Mutex}, + time, +}; use super::{ wifi_hotspot::{default_gateway_wifi, WifiHotstop}, - InterfaceType, NetworkGateway, NetworkInterface, NetworkProviderConfig, - NetworkTraffic, NetworkOutput, + InterfaceType, NetworkGateway, NetworkInterface, NetworkOutput, + NetworkProviderConfig, NetworkTraffic, }; use crate::providers::{ - provider::IntervalProvider, variables::ProviderOutput, + provider::Provider, provider_ref::ProviderResult, + variables::ProviderOutput, }; pub struct NetworkProvider { - pub config: Arc, - abort_handle: Option, - _state: Arc>, + config: NetworkProviderConfig, netinfo: Arc>, } @@ -26,12 +28,45 @@ impl NetworkProvider { config: NetworkProviderConfig, netinfo: Arc>, ) -> NetworkProvider { - NetworkProvider { - config: Arc::new(config), - abort_handle: None, - _state: Arc::new(Mutex::new(())), - netinfo, - } + NetworkProvider { config, netinfo } + } + + async fn provider_output( + self: Arc, + ) -> anyhow::Result { + let mut netinfo = self.netinfo.lock().await; + netinfo.refresh(); + + let interfaces = get_interfaces(); + + let default_interface = netdev::get_default_interface().ok(); + + Ok(ProviderOutput::Network(NetworkOutput { + default_interface: default_interface + .as_ref() + .map(Self::transform_interface), + default_gateway: default_interface + .and_then(|interface| interface.gateway) + .and_then(|gateway| { + default_gateway_wifi() + .map(|wifi| Self::transform_gateway(&gateway, wifi)) + .ok() + }), + interfaces: interfaces + .iter() + .map(Self::transform_interface) + .collect(), + traffic: NetworkTraffic { + received: to_bytes_per_seconds( + get_network_down(&netinfo), + self.config.refresh_interval, + ), + transmitted: to_bytes_per_seconds( + get_network_up(&netinfo), + self.config.refresh_interval, + ), + }, + })) } fn transform_interface( @@ -87,69 +122,25 @@ impl NetworkProvider { } #[async_trait] -impl IntervalProvider for NetworkProvider { - type Config = NetworkProviderConfig; - type State = Mutex; - - fn config(&self) -> Arc { - self.config.clone() - } - - fn state(&self) -> Arc> { - self.netinfo.clone() - } - - fn abort_handle(&self) -> &Option { - &self.abort_handle - } - - fn set_abort_handle(&mut self, abort_handle: AbortHandle) { - self.abort_handle = Some(abort_handle) - } - - async fn get_refreshed_variables( - config: &NetworkProviderConfig, - netinfo: &Mutex, - ) -> anyhow::Result { - let mut netinfo = netinfo.lock().await; - netinfo.refresh(); - - let interfaces = get_interfaces(); - - let default_interface = netdev::get_default_interface().ok(); - - let variables = NetworkOutput { - default_interface: default_interface - .as_ref() - .map(Self::transform_interface), - default_gateway: default_interface - .and_then(|interface| interface.gateway) - .and_then(|gateway| { - default_gateway_wifi() - .map(|wifi| Self::transform_gateway(&gateway, wifi)) - .ok() - }), - interfaces: interfaces - .iter() - .map(Self::transform_interface) - .collect(), - traffic: NetworkTraffic { - received: to_bytes_per_seconds( - get_network_down(&netinfo), - config.refresh_interval, - ), - transmitted: to_bytes_per_seconds( - get_network_up(&netinfo), - config.refresh_interval, - ), - }, - }; - - Ok(ProviderOutput::Network(variables)) +impl Provider for NetworkProvider { + async fn on_start( + self: Arc, + emit_result_tx: mpsc::Sender, + ) { + let mut interval = + time::interval(Duration::from_millis(self.config.refresh_interval)); + + loop { + interval.tick().await; + + emit_result_tx + .send(self.provider_output().await.into()) + .await; + } } } -// Get the total network (down) usage +/// Gets the total network (down) usage. fn get_network_down(req_net: &sysinfo::Networks) -> u64 { // Get the total bytes recieved by every network interface let mut received_total: Vec = Vec::new(); @@ -160,7 +151,7 @@ fn get_network_down(req_net: &sysinfo::Networks) -> u64 { received_total.iter().sum() } -// Get the total network (up) usage +/// Gets the total network (up) usage. fn get_network_up(req_net: &sysinfo::Networks) -> u64 { // Get the total bytes recieved by every network interface let mut transmitted_total: Vec = Vec::new(); diff --git a/packages/desktop/src/providers/provider_ref.rs b/packages/desktop/src/providers/provider_ref.rs index 1a70dc96..b3edf3ca 100644 --- a/packages/desktop/src/providers/provider_ref.rs +++ b/packages/desktop/src/providers/provider_ref.rs @@ -7,18 +7,14 @@ use tauri::{AppHandle, Emitter}; use tokio::{sync::mpsc, task}; use tracing::{info, warn}; -// #[cfg(windows)] -// use super::komorebi::KomorebiProvider; +#[cfg(windows)] +use super::komorebi::KomorebiProvider; use super::{ - battery::BatteryProvider, - config::ProviderConfig, - cpu::CpuProvider, - // host::HostProvider, ip::IpProvider, memory::MemoryProvider, - // network::NetworkProvider, - provider::Provider, - provider_manager::SharedProviderState, - variables::ProviderOutput, - // weather::WeatherProvider, + battery::BatteryProvider, config::ProviderConfig, cpu::CpuProvider, + host::HostProvider, ip::IpProvider, memory::MemoryProvider, + network::NetworkProvider, provider::Provider, + provider_manager::SharedProviderState, variables::ProviderOutput, + weather::WeatherProvider, }; /// Reference to an active provider. @@ -125,24 +121,24 @@ impl ProviderRef { ProviderConfig::Cpu(config) => { Arc::new(CpuProvider::new(config, shared_state.sysinfo.clone())) } - // ProviderConfig::Host(config) => { - // Box::new(HostProvider::new(config, - // shared_state.sysinfo.clone())) } - // ProviderConfig::Ip(config) => Box::new(IpProvider::new(config)), - // #[cfg(windows)] - // ProviderConfig::Komorebi(config) => { - // Box::new(KomorebiProvider::new(config)) - // } - // ProviderConfig::Memory(config) => { - // Box::new(MemoryProvider::new(config, - // shared_state.sysinfo.clone())) } - // ProviderConfig::Network(config) => Box::new(NetworkProvider::new( - // config, - // shared_state.netinfo.clone(), - // )), - // ProviderConfig::Weather(config) => { - // Box::new(WeatherProvider::new(config)) - // } + ProviderConfig::Host(config) => { + Arc::new(HostProvider::new(config, shared_state.sysinfo.clone())) + } + ProviderConfig::Ip(config) => Arc::new(IpProvider::new(config)), + #[cfg(windows)] + ProviderConfig::Komorebi(config) => { + Arc::new(KomorebiProvider::new(config)) + } + ProviderConfig::Memory(config) => { + Arc::new(MemoryProvider::new(config, shared_state.sysinfo.clone())) + } + ProviderConfig::Network(config) => Arc::new(NetworkProvider::new( + config, + shared_state.netinfo.clone(), + )), + ProviderConfig::Weather(config) => { + Arc::new(WeatherProvider::new(config)) + } #[allow(unreachable_patterns)] _ => bail!("Provider not supported on this operating system."), }; diff --git a/packages/desktop/src/providers/variables.rs b/packages/desktop/src/providers/variables.rs index e123ece6..3f7ccea3 100644 --- a/packages/desktop/src/providers/variables.rs +++ b/packages/desktop/src/providers/variables.rs @@ -1,13 +1,10 @@ use serde::Serialize; -// #[cfg(windows)] -// use super::komorebi::KomorebiOutput; +#[cfg(windows)] +use super::komorebi::KomorebiOutput; use super::{ - battery::BatteryOutput, - cpu::CpuOutput, - // host::HostOutput, - // ip::IpOutput, memory::MemoryOutput, network::NetworkOutput, - // weather::WeatherOutput, + battery::BatteryOutput, cpu::CpuOutput, host::HostOutput, ip::IpOutput, + memory::MemoryOutput, network::NetworkOutput, weather::WeatherOutput, }; #[derive(Serialize, Debug, Clone)] @@ -15,11 +12,11 @@ use super::{ pub enum ProviderOutput { Battery(BatteryOutput), Cpu(CpuOutput), - // Host(HostOutput), - // Ip(IpOutput), - // #[cfg(windows)] - // Komorebi(KomorebiOutput), - // Memory(MemoryOutput), - // Network(NetworkOutput), - // Weather(WeatherOutput), + Host(HostOutput), + Ip(IpOutput), + #[cfg(windows)] + Komorebi(KomorebiOutput), + Memory(MemoryOutput), + Network(NetworkOutput), + Weather(WeatherOutput), } diff --git a/packages/desktop/src/providers/weather/config.rs b/packages/desktop/src/providers/weather/config.rs index 3a1e78cc..bc24a2d3 100644 --- a/packages/desktop/src/providers/weather/config.rs +++ b/packages/desktop/src/providers/weather/config.rs @@ -1,7 +1,5 @@ use serde::Deserialize; -use crate::impl_interval_config; - #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct WeatherProviderConfig { @@ -9,5 +7,3 @@ pub struct WeatherProviderConfig { pub latitude: f32, pub longitude: f32, } - -impl_interval_config!(WeatherProviderConfig); diff --git a/packages/desktop/src/providers/weather/provider.rs b/packages/desktop/src/providers/weather/provider.rs index 7191775e..5ede3725 100644 --- a/packages/desktop/src/providers/weather/provider.rs +++ b/packages/desktop/src/providers/weather/provider.rs @@ -1,32 +1,67 @@ -use std::sync::Arc; +use std::{sync::Arc, time::Duration}; use async_trait::async_trait; use reqwest::Client; -use tokio::task::AbortHandle; +use tokio::{sync::mpsc, time}; use super::{ - open_meteo_res::OpenMeteoRes, WeatherProviderConfig, WeatherStatus, - WeatherOutput, + open_meteo_res::OpenMeteoRes, WeatherOutput, WeatherProviderConfig, + WeatherStatus, }; use crate::providers::{ - provider::IntervalProvider, variables::ProviderOutput, + provider::Provider, provider_ref::ProviderResult, + variables::ProviderOutput, }; pub struct WeatherProvider { - pub config: Arc, - abort_handle: Option, - http_client: Arc, + config: WeatherProviderConfig, + http_client: Client, } impl WeatherProvider { pub fn new(config: WeatherProviderConfig) -> WeatherProvider { WeatherProvider { - config: Arc::new(config), - abort_handle: None, - http_client: Arc::new(Client::new()), + config, + http_client: Client::new(), } } + async fn interval_output( + config: &WeatherProviderConfig, + http_client: &Client, + ) -> anyhow::Result { + let res = http_client + .get("https://api.open-meteo.com/v1/forecast") + .query(&[ + ("temperature_unit", "celsius"), + ("latitude", &config.latitude.to_string()), + ("longitude", &config.longitude.to_string()), + ("current_weather", "true"), + ("daily", "sunset,sunrise"), + ("timezone", "auto"), + ]) + .send() + .await? + .json::() + .await?; + + let current_weather = res.current_weather; + let is_daytime = current_weather.is_day == 1; + + Ok(ProviderOutput::Weather(WeatherOutput { + is_daytime, + status: Self::get_weather_status( + current_weather.weather_code, + is_daytime, + ), + celsius_temp: current_weather.temperature, + fahrenheit_temp: Self::celsius_to_fahrenheit( + current_weather.temperature, + ), + wind_speed: current_weather.wind_speed, + })) + } + fn celsius_to_fahrenheit(celsius_temp: f32) -> f32 { return (celsius_temp * 9.) / 5. + 32.; } @@ -71,59 +106,24 @@ impl WeatherProvider { } #[async_trait] -impl IntervalProvider for WeatherProvider { - type Config = WeatherProviderConfig; - type State = Client; - - fn config(&self) -> Arc { - self.config.clone() - } - - fn state(&self) -> Arc { - self.http_client.clone() - } - - fn abort_handle(&self) -> &Option { - &self.abort_handle - } - - fn set_abort_handle(&mut self, abort_handle: AbortHandle) { - self.abort_handle = Some(abort_handle) - } - - async fn get_refreshed_variables( - config: &WeatherProviderConfig, - http_client: &Client, - ) -> anyhow::Result { - let res = http_client - .get("https://api.open-meteo.com/v1/forecast") - .query(&[ - ("temperature_unit", "celsius"), - ("latitude", &config.latitude.to_string()), - ("longitude", &config.longitude.to_string()), - ("current_weather", "true"), - ("daily", "sunset,sunrise"), - ("timezone", "auto"), - ]) - .send() - .await? - .json::() - .await?; +impl Provider for WeatherProvider { + async fn on_start( + self: Arc, + emit_result_tx: mpsc::Sender, + ) { + let mut interval = + time::interval(Duration::from_millis(self.config.refresh_interval)); - let current_weather = res.current_weather; - let is_daytime = current_weather.is_day == 1; + loop { + interval.tick().await; - Ok(ProviderOutput::Weather(WeatherOutput { - is_daytime, - status: Self::get_weather_status( - current_weather.weather_code, - is_daytime, - ), - celsius_temp: current_weather.temperature, - fahrenheit_temp: Self::celsius_to_fahrenheit( - current_weather.temperature, - ), - wind_speed: current_weather.wind_speed, - })) + emit_result_tx + .send( + Self::interval_output(&self.config, &self.http_client) + .await + .into(), + ) + .await; + } } } From b75e9ca4cb2e481ad1afcacd258e0eadae878af1 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Fri, 6 Sep 2024 15:22:56 +0800 Subject: [PATCH 084/138] fix: type errors --- .../desktop/src/providers/memory/provider.rs | 2 +- .../desktop/src/providers/network/provider.rs | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/desktop/src/providers/memory/provider.rs b/packages/desktop/src/providers/memory/provider.rs index 3344406a..7080e4b2 100644 --- a/packages/desktop/src/providers/memory/provider.rs +++ b/packages/desktop/src/providers/memory/provider.rs @@ -61,7 +61,7 @@ impl Provider for MemoryProvider { interval.tick().await; emit_result_tx - .send(Self::interval_output().await.into()) + .send(Self::interval_output(self.sysinfo.clone()).await.into()) .await; } } diff --git a/packages/desktop/src/providers/network/provider.rs b/packages/desktop/src/providers/network/provider.rs index 6fe87c95..57453939 100644 --- a/packages/desktop/src/providers/network/provider.rs +++ b/packages/desktop/src/providers/network/provider.rs @@ -31,10 +31,11 @@ impl NetworkProvider { NetworkProvider { config, netinfo } } - async fn provider_output( - self: Arc, + async fn interval_output( + config: &NetworkProviderConfig, + netinfo: Arc>, ) -> anyhow::Result { - let mut netinfo = self.netinfo.lock().await; + let mut netinfo = netinfo.lock().await; netinfo.refresh(); let interfaces = get_interfaces(); @@ -59,11 +60,11 @@ impl NetworkProvider { traffic: NetworkTraffic { received: to_bytes_per_seconds( get_network_down(&netinfo), - self.config.refresh_interval, + config.refresh_interval, ), transmitted: to_bytes_per_seconds( get_network_up(&netinfo), - self.config.refresh_interval, + config.refresh_interval, ), }, })) @@ -134,7 +135,11 @@ impl Provider for NetworkProvider { interval.tick().await; emit_result_tx - .send(self.provider_output().await.into()) + .send( + Self::interval_output(&self.config, self.netinfo.clone()) + .await + .into(), + ) .await; } } From 7a081f2aa49e0d7a6129c29080f4fec05205d574 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Fri, 6 Sep 2024 15:51:05 +0800 Subject: [PATCH 085/138] feat: add back stop/refresh channels; remove Arc usage; simplify tokio::select --- .../desktop/src/providers/battery/provider.rs | 5 +- .../desktop/src/providers/cpu/provider.rs | 5 +- .../desktop/src/providers/host/provider.rs | 5 +- packages/desktop/src/providers/ip/provider.rs | 5 +- .../src/providers/komorebi/provider.rs | 7 +- .../desktop/src/providers/memory/provider.rs | 5 +- .../desktop/src/providers/network/provider.rs | 5 +- packages/desktop/src/providers/provider.rs | 9 +- .../desktop/src/providers/provider_manager.rs | 2 +- .../desktop/src/providers/provider_ref.rs | 99 +++++++++++++------ .../desktop/src/providers/weather/provider.rs | 5 +- 11 files changed, 80 insertions(+), 72 deletions(-) diff --git a/packages/desktop/src/providers/battery/provider.rs b/packages/desktop/src/providers/battery/provider.rs index 5301d87c..ccceb308 100644 --- a/packages/desktop/src/providers/battery/provider.rs +++ b/packages/desktop/src/providers/battery/provider.rs @@ -63,10 +63,7 @@ impl BatteryProvider { #[async_trait] impl Provider for BatteryProvider { - async fn on_start( - self: Arc, - emit_result_tx: mpsc::Sender, - ) { + async fn on_start(&self, emit_result_tx: mpsc::Sender) { let mut interval = time::interval(Duration::from_millis(self.config.refresh_interval)); diff --git a/packages/desktop/src/providers/cpu/provider.rs b/packages/desktop/src/providers/cpu/provider.rs index 5c915111..9f0a10ac 100644 --- a/packages/desktop/src/providers/cpu/provider.rs +++ b/packages/desktop/src/providers/cpu/provider.rs @@ -29,10 +29,7 @@ impl CpuProvider { #[async_trait] impl Provider for CpuProvider { - async fn on_start( - self: Arc, - emit_result_tx: mpsc::Sender, - ) { + async fn on_start(&self, emit_result_tx: mpsc::Sender) { let mut interval = time::interval(Duration::from_millis(self.config.refresh_interval)); diff --git a/packages/desktop/src/providers/host/provider.rs b/packages/desktop/src/providers/host/provider.rs index 1e4623c6..cf48e8ba 100644 --- a/packages/desktop/src/providers/host/provider.rs +++ b/packages/desktop/src/providers/host/provider.rs @@ -40,10 +40,7 @@ impl HostProvider { #[async_trait] impl Provider for HostProvider { - async fn on_start( - self: Arc, - emit_result_tx: mpsc::Sender, - ) { + async fn on_start(&self, emit_result_tx: mpsc::Sender) { let mut interval = time::interval(Duration::from_millis(self.config.refresh_interval)); diff --git a/packages/desktop/src/providers/ip/provider.rs b/packages/desktop/src/providers/ip/provider.rs index 8be390d5..00dfe335 100644 --- a/packages/desktop/src/providers/ip/provider.rs +++ b/packages/desktop/src/providers/ip/provider.rs @@ -54,10 +54,7 @@ impl IpProvider { #[async_trait] impl Provider for IpProvider { - async fn on_start( - self: Arc, - emit_result_tx: mpsc::Sender, - ) { + async fn on_start(&self, emit_result_tx: mpsc::Sender) { let mut interval = time::interval(Duration::from_millis(self.config.refresh_interval)); diff --git a/packages/desktop/src/providers/komorebi/provider.rs b/packages/desktop/src/providers/komorebi/provider.rs index 17fee2b8..ea9b4a70 100644 --- a/packages/desktop/src/providers/komorebi/provider.rs +++ b/packages/desktop/src/providers/komorebi/provider.rs @@ -33,7 +33,7 @@ impl KomorebiProvider { } async fn create_socket( - self: Arc, + &self, emit_result_tx: Sender, ) -> anyhow::Result<()> { let socket = komorebi_client::subscribe(SOCKET_NAME) @@ -177,10 +177,7 @@ impl KomorebiProvider { #[async_trait] impl Provider for KomorebiProvider { - async fn on_start( - self: Arc, - emit_result_tx: Sender, - ) { + async fn on_start(&self, emit_result_tx: Sender) { if let Err(err) = self.create_socket(emit_result_tx.clone()).await { emit_result_tx.send(Err(err).into()).await; } diff --git a/packages/desktop/src/providers/memory/provider.rs b/packages/desktop/src/providers/memory/provider.rs index 7080e4b2..956e4759 100644 --- a/packages/desktop/src/providers/memory/provider.rs +++ b/packages/desktop/src/providers/memory/provider.rs @@ -50,10 +50,7 @@ impl MemoryProvider { #[async_trait] impl Provider for MemoryProvider { - async fn on_start( - self: Arc, - emit_result_tx: mpsc::Sender, - ) { + async fn on_start(&self, emit_result_tx: mpsc::Sender) { let mut interval = time::interval(Duration::from_millis(self.config.refresh_interval)); diff --git a/packages/desktop/src/providers/network/provider.rs b/packages/desktop/src/providers/network/provider.rs index 57453939..4f75dfdc 100644 --- a/packages/desktop/src/providers/network/provider.rs +++ b/packages/desktop/src/providers/network/provider.rs @@ -124,10 +124,7 @@ impl NetworkProvider { #[async_trait] impl Provider for NetworkProvider { - async fn on_start( - self: Arc, - emit_result_tx: mpsc::Sender, - ) { + async fn on_start(&self, emit_result_tx: mpsc::Sender) { let mut interval = time::interval(Duration::from_millis(self.config.refresh_interval)); diff --git a/packages/desktop/src/providers/provider.rs b/packages/desktop/src/providers/provider.rs index e8185599..b667b690 100644 --- a/packages/desktop/src/providers/provider.rs +++ b/packages/desktop/src/providers/provider.rs @@ -1,5 +1,3 @@ -use std::sync::Arc; - use async_trait::async_trait; use tokio::sync::mpsc::Sender; @@ -8,13 +6,10 @@ use super::provider_ref::ProviderResult; #[async_trait] pub trait Provider: Send + Sync { /// Callback for when the provider is started. - async fn on_start( - self: Arc, - emit_result_tx: Sender, - ); + async fn on_start(&self, emit_result_tx: Sender); /// Callback for when the provider is stopped. - async fn on_stop(self: Arc) { + async fn on_stop(&self) { // No-op by default. } } diff --git a/packages/desktop/src/providers/provider_manager.rs b/packages/desktop/src/providers/provider_manager.rs index 3b6b6960..f066b9e7 100644 --- a/packages/desktop/src/providers/provider_manager.rs +++ b/packages/desktop/src/providers/provider_manager.rs @@ -60,7 +60,7 @@ impl ProviderManager { &self.app_handle, config_hash.clone(), config, - &self.shared_state, + self.shared_state.clone(), ) .await?; diff --git a/packages/desktop/src/providers/provider_ref.rs b/packages/desktop/src/providers/provider_ref.rs index b3edf3ca..aeaf634c 100644 --- a/packages/desktop/src/providers/provider_ref.rs +++ b/packages/desktop/src/providers/provider_ref.rs @@ -19,17 +19,18 @@ use super::{ /// Reference to an active provider. pub struct ProviderRef { - pub config_hash: String, - pub cache: Option, - provider: Arc, + config_hash: String, + cache: Option, emit_result_tx: mpsc::Sender, + refresh_tx: mpsc::Sender<()>, + stop_tx: mpsc::Sender<()>, } /// Cache for provider output. #[derive(Debug, Clone)] pub struct ProviderCache { - pub timestamp: Instant, - pub output: Box, + timestamp: Instant, + output: Box, } /// Provider output/error emitted to frontend clients. @@ -59,10 +60,8 @@ impl ProviderRef { app_handle: &AppHandle, config_hash: String, config: ProviderConfig, - shared_state: &SharedProviderState, + shared_state: SharedProviderState, ) -> anyhow::Result { - let provider = Self::create_provider(config, shared_state)?; - let (emit_result_tx, mut emit_result_rx) = mpsc::channel::(1); @@ -74,7 +73,7 @@ impl ProviderRef { let output = Box::new(output); let payload = json!({ - "config_hash": config_hash_clone.clone(), + "configHash": config_hash_clone.clone(), "result": *output.clone(), }); @@ -95,49 +94,90 @@ impl ProviderRef { } }); - info!("Starting provider: {}", config_hash); - let provider_clone = provider.clone(); - let emit_result_tx_clone = emit_result_tx.clone(); - task::spawn(async move { - provider_clone.on_start(emit_result_tx_clone).await; - }); + let (refresh_tx, refresh_rx) = mpsc::channel::<()>(1); + let (stop_tx, stop_rx) = mpsc::channel::<()>(1); + + Self::start_provider( + config, + shared_state, + emit_result_tx.clone(), + refresh_rx, + stop_rx, + ); Ok(Self { config_hash, cache: None, - provider, emit_result_tx, + stop_tx, + refresh_tx, }) } + /// Starts the provider in a separate task. + fn start_provider( + config: ProviderConfig, + shared_state: SharedProviderState, + emit_result_tx: mpsc::Sender, + mut refresh_rx: mpsc::Receiver<()>, + mut stop_rx: mpsc::Receiver<()>, + ) { + task::spawn(async move { + // TODO: Remove unwrap. + let provider = Self::create_provider(config, shared_state).unwrap(); + + // TODO: Add arc `should_stop` to be passed to `on_start`. + + let start = provider.on_start(emit_result_tx); + tokio::pin!(start); + + loop { + tokio::select! { + // Default match arm which handles initialization of the provider. + // This has a precondition to avoid running again on refresh. + _ = start => break, + + // On stop, perform any necessary clean up and exit the loop. + Some(_) = stop_rx.recv() => { + // info!("Stopping provider: {}", config_hash); + _ = provider.on_stop().await; + break; + }, + } + } + + // info!("Provider stopped: {}", config_hash); + }); + } + fn create_provider( config: ProviderConfig, - shared_state: &SharedProviderState, - ) -> anyhow::Result> { - let provider: Arc = match config { + shared_state: SharedProviderState, + ) -> anyhow::Result> { + let provider: Box = match config { ProviderConfig::Battery(config) => { - Arc::new(BatteryProvider::new(config)?) + Box::new(BatteryProvider::new(config)?) } ProviderConfig::Cpu(config) => { - Arc::new(CpuProvider::new(config, shared_state.sysinfo.clone())) + Box::new(CpuProvider::new(config, shared_state.sysinfo.clone())) } ProviderConfig::Host(config) => { - Arc::new(HostProvider::new(config, shared_state.sysinfo.clone())) + Box::new(HostProvider::new(config, shared_state.sysinfo.clone())) } - ProviderConfig::Ip(config) => Arc::new(IpProvider::new(config)), + ProviderConfig::Ip(config) => Box::new(IpProvider::new(config)), #[cfg(windows)] ProviderConfig::Komorebi(config) => { - Arc::new(KomorebiProvider::new(config)) + Box::new(KomorebiProvider::new(config)) } ProviderConfig::Memory(config) => { - Arc::new(MemoryProvider::new(config, shared_state.sysinfo.clone())) + Box::new(MemoryProvider::new(config, shared_state.sysinfo.clone())) } - ProviderConfig::Network(config) => Arc::new(NetworkProvider::new( + ProviderConfig::Network(config) => Box::new(NetworkProvider::new( config, shared_state.netinfo.clone(), )), ProviderConfig::Weather(config) => { - Arc::new(WeatherProvider::new(config)) + Box::new(WeatherProvider::new(config)) } #[allow(unreachable_patterns)] _ => bail!("Provider not supported on this operating system."), @@ -170,10 +210,7 @@ impl ProviderRef { /// /// This triggers any necessary cleanup. pub async fn stop(&mut self) -> anyhow::Result<()> { - info!("Stopping provider: {}", self.config_hash); - _ = self.provider.clone().on_stop().await; - info!("Provider stopped: {}", self.config_hash); - + self.stop_tx.send(()).await?; Ok(()) } } diff --git a/packages/desktop/src/providers/weather/provider.rs b/packages/desktop/src/providers/weather/provider.rs index 5ede3725..b9c99975 100644 --- a/packages/desktop/src/providers/weather/provider.rs +++ b/packages/desktop/src/providers/weather/provider.rs @@ -107,10 +107,7 @@ impl WeatherProvider { #[async_trait] impl Provider for WeatherProvider { - async fn on_start( - self: Arc, - emit_result_tx: mpsc::Sender, - ) { + async fn on_start(&self, emit_result_tx: mpsc::Sender) { let mut interval = time::interval(Duration::from_millis(self.config.refresh_interval)); From 72205d38c0f7f5a37a2b421d99ca5a2254466d02 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Fri, 6 Sep 2024 16:44:09 +0800 Subject: [PATCH 086/138] feat: wip shared interval provider impl --- .../desktop/src/providers/battery/provider.rs | 2 +- .../desktop/src/providers/cpu/provider.rs | 2 +- .../desktop/src/providers/host/provider.rs | 2 +- packages/desktop/src/providers/ip/provider.rs | 2 +- .../src/providers/komorebi/provider.rs | 2 +- .../desktop/src/providers/memory/provider.rs | 33 ++++++----------- .../desktop/src/providers/network/provider.rs | 2 +- packages/desktop/src/providers/provider.rs | 37 +++++++++++++++++-- .../desktop/src/providers/provider_manager.rs | 2 +- .../desktop/src/providers/provider_ref.rs | 25 ++++++------- .../desktop/src/providers/weather/provider.rs | 2 +- 11 files changed, 64 insertions(+), 47 deletions(-) diff --git a/packages/desktop/src/providers/battery/provider.rs b/packages/desktop/src/providers/battery/provider.rs index ccceb308..f7f584bc 100644 --- a/packages/desktop/src/providers/battery/provider.rs +++ b/packages/desktop/src/providers/battery/provider.rs @@ -63,7 +63,7 @@ impl BatteryProvider { #[async_trait] impl Provider for BatteryProvider { - async fn on_start(&self, emit_result_tx: mpsc::Sender) { + async fn run(&self, emit_result_tx: mpsc::Sender) { let mut interval = time::interval(Duration::from_millis(self.config.refresh_interval)); diff --git a/packages/desktop/src/providers/cpu/provider.rs b/packages/desktop/src/providers/cpu/provider.rs index 9f0a10ac..9b5644fb 100644 --- a/packages/desktop/src/providers/cpu/provider.rs +++ b/packages/desktop/src/providers/cpu/provider.rs @@ -29,7 +29,7 @@ impl CpuProvider { #[async_trait] impl Provider for CpuProvider { - async fn on_start(&self, emit_result_tx: mpsc::Sender) { + async fn run(&self, emit_result_tx: mpsc::Sender) { let mut interval = time::interval(Duration::from_millis(self.config.refresh_interval)); diff --git a/packages/desktop/src/providers/host/provider.rs b/packages/desktop/src/providers/host/provider.rs index cf48e8ba..2659674a 100644 --- a/packages/desktop/src/providers/host/provider.rs +++ b/packages/desktop/src/providers/host/provider.rs @@ -40,7 +40,7 @@ impl HostProvider { #[async_trait] impl Provider for HostProvider { - async fn on_start(&self, emit_result_tx: mpsc::Sender) { + async fn run(&self, emit_result_tx: mpsc::Sender) { let mut interval = time::interval(Duration::from_millis(self.config.refresh_interval)); diff --git a/packages/desktop/src/providers/ip/provider.rs b/packages/desktop/src/providers/ip/provider.rs index 00dfe335..e794cb15 100644 --- a/packages/desktop/src/providers/ip/provider.rs +++ b/packages/desktop/src/providers/ip/provider.rs @@ -54,7 +54,7 @@ impl IpProvider { #[async_trait] impl Provider for IpProvider { - async fn on_start(&self, emit_result_tx: mpsc::Sender) { + async fn run(&self, emit_result_tx: mpsc::Sender) { let mut interval = time::interval(Duration::from_millis(self.config.refresh_interval)); diff --git a/packages/desktop/src/providers/komorebi/provider.rs b/packages/desktop/src/providers/komorebi/provider.rs index ea9b4a70..4783b635 100644 --- a/packages/desktop/src/providers/komorebi/provider.rs +++ b/packages/desktop/src/providers/komorebi/provider.rs @@ -177,7 +177,7 @@ impl KomorebiProvider { #[async_trait] impl Provider for KomorebiProvider { - async fn on_start(&self, emit_result_tx: Sender) { + async fn run(&self, emit_result_tx: Sender) { if let Err(err) = self.create_socket(emit_result_tx.clone()).await { emit_result_tx.send(Err(err).into()).await; } diff --git a/packages/desktop/src/providers/memory/provider.rs b/packages/desktop/src/providers/memory/provider.rs index 956e4759..487302f3 100644 --- a/packages/desktop/src/providers/memory/provider.rs +++ b/packages/desktop/src/providers/memory/provider.rs @@ -1,16 +1,12 @@ -use std::{sync::Arc, time::Duration}; +use std::sync::Arc; use async_trait::async_trait; use sysinfo::System; -use tokio::{ - sync::{mpsc, Mutex}, - time, -}; +use tokio::sync::Mutex; use super::{MemoryOutput, MemoryProviderConfig}; use crate::providers::{ - provider::Provider, provider_ref::ProviderResult, - variables::ProviderOutput, + provider::IntervalProvider, variables::ProviderOutput, }; pub struct MemoryProvider { @@ -26,10 +22,8 @@ impl MemoryProvider { MemoryProvider { config, sysinfo } } - async fn interval_output( - sysinfo: Arc>, - ) -> anyhow::Result { - let mut sysinfo = sysinfo.lock().await; + async fn run_interval(&self) -> anyhow::Result { + let mut sysinfo = self.sysinfo.lock().await; sysinfo.refresh_memory(); let usage = (sysinfo.used_memory() as f32 @@ -49,17 +43,12 @@ impl MemoryProvider { } #[async_trait] -impl Provider for MemoryProvider { - async fn on_start(&self, emit_result_tx: mpsc::Sender) { - let mut interval = - time::interval(Duration::from_millis(self.config.refresh_interval)); - - loop { - interval.tick().await; +impl IntervalProvider for MemoryProvider { + fn refresh_interval_ms(&self) -> u64 { + self.config.refresh_interval + } - emit_result_tx - .send(Self::interval_output(self.sysinfo.clone()).await.into()) - .await; - } + async fn run_interval(&self) -> anyhow::Result { + self.run_interval().await } } diff --git a/packages/desktop/src/providers/network/provider.rs b/packages/desktop/src/providers/network/provider.rs index 4f75dfdc..c624ff4b 100644 --- a/packages/desktop/src/providers/network/provider.rs +++ b/packages/desktop/src/providers/network/provider.rs @@ -124,7 +124,7 @@ impl NetworkProvider { #[async_trait] impl Provider for NetworkProvider { - async fn on_start(&self, emit_result_tx: mpsc::Sender) { + async fn run(&self, emit_result_tx: mpsc::Sender) { let mut interval = time::interval(Duration::from_millis(self.config.refresh_interval)); diff --git a/packages/desktop/src/providers/provider.rs b/packages/desktop/src/providers/provider.rs index b667b690..9f0cdacc 100644 --- a/packages/desktop/src/providers/provider.rs +++ b/packages/desktop/src/providers/provider.rs @@ -1,15 +1,46 @@ +use std::time::Duration; + use async_trait::async_trait; -use tokio::sync::mpsc::Sender; +use tokio::{sync::mpsc::Sender, time}; +use tracing::error; -use super::provider_ref::ProviderResult; +use super::{provider_ref::ProviderResult, variables::ProviderOutput}; #[async_trait] pub trait Provider: Send + Sync { /// Callback for when the provider is started. - async fn on_start(&self, emit_result_tx: Sender); + async fn run(&self, emit_result_tx: Sender); /// Callback for when the provider is stopped. async fn on_stop(&self) { // No-op by default. } } + +#[async_trait] +pub trait IntervalProvider { + /// Refresh interval in milliseconds. + fn refresh_interval_ms(&self) -> u64; + + /// Callback for when the provider is started. + async fn run_interval(&self) -> anyhow::Result; +} + +#[async_trait] +impl Provider for T { + async fn run(&self, emit_result_tx: Sender) { + let mut interval = + time::interval(Duration::from_millis(self.refresh_interval_ms())); + + loop { + interval.tick().await; + + let res = + emit_result_tx.send(self.run_interval().await.into()).await; + + if let Err(err) = res { + error!("Error sending provider result: {:?}", err); + } + } + } +} diff --git a/packages/desktop/src/providers/provider_manager.rs b/packages/desktop/src/providers/provider_manager.rs index f066b9e7..e3369d0a 100644 --- a/packages/desktop/src/providers/provider_manager.rs +++ b/packages/desktop/src/providers/provider_manager.rs @@ -58,8 +58,8 @@ impl ProviderManager { let provider_ref = ProviderRef::new( &self.app_handle, - config_hash.clone(), config, + config_hash.clone(), self.shared_state.clone(), ) .await?; diff --git a/packages/desktop/src/providers/provider_ref.rs b/packages/desktop/src/providers/provider_ref.rs index aeaf634c..640a88ce 100644 --- a/packages/desktop/src/providers/provider_ref.rs +++ b/packages/desktop/src/providers/provider_ref.rs @@ -22,7 +22,6 @@ pub struct ProviderRef { config_hash: String, cache: Option, emit_result_tx: mpsc::Sender, - refresh_tx: mpsc::Sender<()>, stop_tx: mpsc::Sender<()>, } @@ -58,8 +57,8 @@ impl ProviderRef { /// Creates a new `ProviderRef` instance. pub async fn new( app_handle: &AppHandle, - config_hash: String, config: ProviderConfig, + config_hash: String, shared_state: SharedProviderState, ) -> anyhow::Result { let (emit_result_tx, mut emit_result_rx) = @@ -94,14 +93,13 @@ impl ProviderRef { } }); - let (refresh_tx, refresh_rx) = mpsc::channel::<()>(1); let (stop_tx, stop_rx) = mpsc::channel::<()>(1); Self::start_provider( config, + config_hash.clone(), shared_state, emit_result_tx.clone(), - refresh_rx, stop_rx, ); @@ -110,43 +108,42 @@ impl ProviderRef { cache: None, emit_result_tx, stop_tx, - refresh_tx, }) } /// Starts the provider in a separate task. fn start_provider( config: ProviderConfig, + config_hash: String, shared_state: SharedProviderState, emit_result_tx: mpsc::Sender, - mut refresh_rx: mpsc::Receiver<()>, mut stop_rx: mpsc::Receiver<()>, ) { task::spawn(async move { // TODO: Remove unwrap. let provider = Self::create_provider(config, shared_state).unwrap(); - // TODO: Add arc `should_stop` to be passed to `on_start`. + // TODO: Add arc `should_stop` to be passed to `run`. - let start = provider.on_start(emit_result_tx); - tokio::pin!(start); + let run = provider.run(emit_result_tx); + tokio::pin!(run); + // Ref: https://tokio.rs/tokio/tutorial/select#resuming-an-async-operation loop { tokio::select! { - // Default match arm which handles initialization of the provider. - // This has a precondition to avoid running again on refresh. - _ = start => break, + // Default match arm which continuously runs the provider. + _ = run => break, // On stop, perform any necessary clean up and exit the loop. Some(_) = stop_rx.recv() => { - // info!("Stopping provider: {}", config_hash); + info!("Stopping provider: {}", config_hash); _ = provider.on_stop().await; break; }, } } - // info!("Provider stopped: {}", config_hash); + info!("Provider stopped: {}", config_hash); }); } diff --git a/packages/desktop/src/providers/weather/provider.rs b/packages/desktop/src/providers/weather/provider.rs index b9c99975..efc1372b 100644 --- a/packages/desktop/src/providers/weather/provider.rs +++ b/packages/desktop/src/providers/weather/provider.rs @@ -107,7 +107,7 @@ impl WeatherProvider { #[async_trait] impl Provider for WeatherProvider { - async fn on_start(&self, emit_result_tx: mpsc::Sender) { + async fn run(&self, emit_result_tx: mpsc::Sender) { let mut interval = time::interval(Duration::from_millis(self.config.refresh_interval)); From 74432ebacf3387c6c4e95be5f6d76daad58088fc Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Fri, 6 Sep 2024 17:21:50 +0800 Subject: [PATCH 087/138] refactor: consume `impl_interval_provider` macro in all interval providers --- .../desktop/src/providers/battery/provider.rs | 38 +++--------- .../desktop/src/providers/cpu/provider.rs | 57 +++++++---------- .../desktop/src/providers/host/provider.rs | 42 +++---------- packages/desktop/src/providers/ip/provider.rs | 36 +++-------- .../src/providers/komorebi/provider.rs | 1 - .../desktop/src/providers/memory/provider.rs | 20 +++--- .../desktop/src/providers/network/provider.rs | 48 ++++----------- packages/desktop/src/providers/provider.rs | 61 ++++++++++--------- .../desktop/src/providers/provider_ref.rs | 4 +- .../desktop/src/providers/weather/provider.rs | 45 ++++---------- 10 files changed, 118 insertions(+), 234 deletions(-) diff --git a/packages/desktop/src/providers/battery/provider.rs b/packages/desktop/src/providers/battery/provider.rs index f7f584bc..71ec547b 100644 --- a/packages/desktop/src/providers/battery/provider.rs +++ b/packages/desktop/src/providers/battery/provider.rs @@ -1,7 +1,4 @@ -use std::{sync::Arc, time::Duration}; - use anyhow::Context; -use async_trait::async_trait; use starship_battery::{ units::{ electric_potential::volt, power::watt, ratio::percent, @@ -9,12 +6,10 @@ use starship_battery::{ }, Manager, State, }; -use tokio::{sync::mpsc, time}; use super::{BatteryOutput, BatteryProviderConfig}; -use crate::providers::{ - provider::Provider, provider_ref::ProviderResult, - variables::ProviderOutput, +use crate::{ + impl_interval_provider, providers::variables::ProviderOutput, }; pub struct BatteryProvider { @@ -32,12 +27,13 @@ impl BatteryProvider { }) } - /// Battery manager from `starship_battery` is not thread-safe, so it - /// requires its own non-async function. - pub fn get_variables( - manager: &Manager, - ) -> anyhow::Result { - let battery = manager + fn refresh_interval_ms(&self) -> u64 { + self.config.refresh_interval + } + + async fn run_interval(&self) -> anyhow::Result { + let battery = self + .battery_manager .batteries() .and_then(|mut batteries| batteries.nth(0).transpose()) .unwrap_or(None) @@ -61,18 +57,4 @@ impl BatteryProvider { } } -#[async_trait] -impl Provider for BatteryProvider { - async fn run(&self, emit_result_tx: mpsc::Sender) { - let mut interval = - time::interval(Duration::from_millis(self.config.refresh_interval)); - - loop { - interval.tick().await; - emit_result_tx - .send(Self::get_variables(&self.battery_manager).into()) - .await - .unwrap(); - } - } -} +impl_interval_provider!(BatteryProvider); diff --git a/packages/desktop/src/providers/cpu/provider.rs b/packages/desktop/src/providers/cpu/provider.rs index 9b5644fb..f4d62a64 100644 --- a/packages/desktop/src/providers/cpu/provider.rs +++ b/packages/desktop/src/providers/cpu/provider.rs @@ -1,16 +1,11 @@ -use std::{sync::Arc, time::Duration}; +use std::sync::Arc; -use async_trait::async_trait; use sysinfo::System; -use tokio::{ - sync::{mpsc, Mutex}, - time, -}; +use tokio::sync::Mutex; use super::{CpuOutput, CpuProviderConfig}; -use crate::providers::{ - provider::Provider, provider_ref::ProviderResult, - variables::ProviderOutput, +use crate::{ + impl_interval_provider, providers::variables::ProviderOutput, }; pub struct CpuProvider { @@ -25,31 +20,25 @@ impl CpuProvider { ) -> CpuProvider { CpuProvider { config, sysinfo } } -} -#[async_trait] -impl Provider for CpuProvider { - async fn run(&self, emit_result_tx: mpsc::Sender) { - let mut interval = - time::interval(Duration::from_millis(self.config.refresh_interval)); - - loop { - interval.tick().await; - - let mut sysinfo = self.sysinfo.lock().await; - sysinfo.refresh_cpu(); - - let res = Ok(ProviderOutput::Cpu(CpuOutput { - usage: sysinfo.global_cpu_info().cpu_usage(), - frequency: sysinfo.global_cpu_info().frequency(), - logical_core_count: sysinfo.cpus().len(), - physical_core_count: sysinfo - .physical_core_count() - .unwrap_or(sysinfo.cpus().len()), - vendor: sysinfo.global_cpu_info().vendor_id().into(), - })); - - emit_result_tx.send(res.into()).await; - } + fn refresh_interval_ms(&self) -> u64 { + self.config.refresh_interval + } + + async fn run_interval(&self) -> anyhow::Result { + let mut sysinfo = self.sysinfo.lock().await; + sysinfo.refresh_cpu(); + + Ok(ProviderOutput::Cpu(CpuOutput { + usage: sysinfo.global_cpu_info().cpu_usage(), + frequency: sysinfo.global_cpu_info().frequency(), + logical_core_count: sysinfo.cpus().len(), + physical_core_count: sysinfo + .physical_core_count() + .unwrap_or(sysinfo.cpus().len()), + vendor: sysinfo.global_cpu_info().vendor_id().into(), + })) } } + +impl_interval_provider!(CpuProvider); diff --git a/packages/desktop/src/providers/host/provider.rs b/packages/desktop/src/providers/host/provider.rs index 2659674a..03caa53f 100644 --- a/packages/desktop/src/providers/host/provider.rs +++ b/packages/desktop/src/providers/host/provider.rs @@ -1,32 +1,24 @@ -use std::{sync::Arc, time::Duration}; - -use async_trait::async_trait; use sysinfo::System; -use tokio::{ - sync::{mpsc, Mutex}, - time, -}; use super::{HostOutput, HostProviderConfig}; -use crate::providers::{ - provider::Provider, provider_ref::ProviderResult, - variables::ProviderOutput, +use crate::{ + impl_interval_provider, providers::variables::ProviderOutput, }; pub struct HostProvider { config: HostProviderConfig, - sysinfo: Arc>, } impl HostProvider { - pub fn new( - config: HostProviderConfig, - sysinfo: Arc>, - ) -> HostProvider { - HostProvider { config, sysinfo } + pub fn new(config: HostProviderConfig) -> HostProvider { + HostProvider { config } + } + + fn refresh_interval_ms(&self) -> u64 { + self.config.refresh_interval } - async fn interval_output() -> anyhow::Result { + async fn run_interval(&self) -> anyhow::Result { Ok(ProviderOutput::Host(HostOutput { hostname: System::host_name(), os_name: System::name(), @@ -38,18 +30,4 @@ impl HostProvider { } } -#[async_trait] -impl Provider for HostProvider { - async fn run(&self, emit_result_tx: mpsc::Sender) { - let mut interval = - time::interval(Duration::from_millis(self.config.refresh_interval)); - - loop { - interval.tick().await; - - emit_result_tx - .send(Self::interval_output().await.into()) - .await; - } - } -} +impl_interval_provider!(HostProvider); diff --git a/packages/desktop/src/providers/ip/provider.rs b/packages/desktop/src/providers/ip/provider.rs index e794cb15..fb1c116e 100644 --- a/packages/desktop/src/providers/ip/provider.rs +++ b/packages/desktop/src/providers/ip/provider.rs @@ -1,14 +1,9 @@ -use std::{sync::Arc, time::Duration}; - use anyhow::Context; -use async_trait::async_trait; use reqwest::Client; -use tokio::{sync::mpsc, time}; use super::{ipinfo_res::IpinfoRes, IpOutput, IpProviderConfig}; -use crate::providers::{ - provider::Provider, provider_ref::ProviderResult, - variables::ProviderOutput, +use crate::{ + impl_interval_provider, providers::variables::ProviderOutput, }; pub struct IpProvider { @@ -24,10 +19,13 @@ impl IpProvider { } } - async fn interval_output( - http_client: &Client, - ) -> anyhow::Result { - let res = http_client + fn refresh_interval_ms(&self) -> u64 { + self.config.refresh_interval + } + + async fn run_interval(&self) -> anyhow::Result { + let res = self + .http_client .get("https://ipinfo.io/json") .send() .await? @@ -52,18 +50,4 @@ impl IpProvider { } } -#[async_trait] -impl Provider for IpProvider { - async fn run(&self, emit_result_tx: mpsc::Sender) { - let mut interval = - time::interval(Duration::from_millis(self.config.refresh_interval)); - - loop { - interval.tick().await; - - emit_result_tx - .send(Self::interval_output(&self.http_client).await.into()) - .await; - } - } -} +impl_interval_provider!(IpProvider); diff --git a/packages/desktop/src/providers/komorebi/provider.rs b/packages/desktop/src/providers/komorebi/provider.rs index 4783b635..adaf6cc3 100644 --- a/packages/desktop/src/providers/komorebi/provider.rs +++ b/packages/desktop/src/providers/komorebi/provider.rs @@ -1,6 +1,5 @@ use std::{ io::{BufReader, Read}, - sync::Arc, time::Duration, }; diff --git a/packages/desktop/src/providers/memory/provider.rs b/packages/desktop/src/providers/memory/provider.rs index 487302f3..14a58532 100644 --- a/packages/desktop/src/providers/memory/provider.rs +++ b/packages/desktop/src/providers/memory/provider.rs @@ -1,12 +1,11 @@ use std::sync::Arc; -use async_trait::async_trait; use sysinfo::System; use tokio::sync::Mutex; use super::{MemoryOutput, MemoryProviderConfig}; -use crate::providers::{ - provider::IntervalProvider, variables::ProviderOutput, +use crate::{ + impl_interval_provider, providers::variables::ProviderOutput, }; pub struct MemoryProvider { @@ -22,6 +21,10 @@ impl MemoryProvider { MemoryProvider { config, sysinfo } } + fn refresh_interval_ms(&self) -> u64 { + self.config.refresh_interval + } + async fn run_interval(&self) -> anyhow::Result { let mut sysinfo = self.sysinfo.lock().await; sysinfo.refresh_memory(); @@ -42,13 +45,4 @@ impl MemoryProvider { } } -#[async_trait] -impl IntervalProvider for MemoryProvider { - fn refresh_interval_ms(&self) -> u64 { - self.config.refresh_interval - } - - async fn run_interval(&self) -> anyhow::Result { - self.run_interval().await - } -} +impl_interval_provider!(MemoryProvider); diff --git a/packages/desktop/src/providers/network/provider.rs b/packages/desktop/src/providers/network/provider.rs index c624ff4b..a1ac2827 100644 --- a/packages/desktop/src/providers/network/provider.rs +++ b/packages/desktop/src/providers/network/provider.rs @@ -1,21 +1,16 @@ -use std::{sync::Arc, time::Duration}; +use std::sync::Arc; -use async_trait::async_trait; use netdev::interface::get_interfaces; use sysinfo::Networks; -use tokio::{ - sync::{mpsc, Mutex}, - time, -}; +use tokio::sync::Mutex; use super::{ wifi_hotspot::{default_gateway_wifi, WifiHotstop}, InterfaceType, NetworkGateway, NetworkInterface, NetworkOutput, NetworkProviderConfig, NetworkTraffic, }; -use crate::providers::{ - provider::Provider, provider_ref::ProviderResult, - variables::ProviderOutput, +use crate::{ + impl_interval_provider, providers::variables::ProviderOutput, }; pub struct NetworkProvider { @@ -31,11 +26,12 @@ impl NetworkProvider { NetworkProvider { config, netinfo } } - async fn interval_output( - config: &NetworkProviderConfig, - netinfo: Arc>, - ) -> anyhow::Result { - let mut netinfo = netinfo.lock().await; + fn refresh_interval_ms(&self) -> u64 { + self.config.refresh_interval + } + + async fn run_interval(&self) -> anyhow::Result { + let mut netinfo = self.netinfo.lock().await; netinfo.refresh(); let interfaces = get_interfaces(); @@ -60,11 +56,11 @@ impl NetworkProvider { traffic: NetworkTraffic { received: to_bytes_per_seconds( get_network_down(&netinfo), - config.refresh_interval, + self.config.refresh_interval, ), transmitted: to_bytes_per_seconds( get_network_up(&netinfo), - config.refresh_interval, + self.config.refresh_interval, ), }, })) @@ -122,25 +118,7 @@ impl NetworkProvider { } } -#[async_trait] -impl Provider for NetworkProvider { - async fn run(&self, emit_result_tx: mpsc::Sender) { - let mut interval = - time::interval(Duration::from_millis(self.config.refresh_interval)); - - loop { - interval.tick().await; - - emit_result_tx - .send( - Self::interval_output(&self.config, self.netinfo.clone()) - .await - .into(), - ) - .await; - } - } -} +impl_interval_provider!(NetworkProvider); /// Gets the total network (down) usage. fn get_network_down(req_net: &sysinfo::Networks) -> u64 { diff --git a/packages/desktop/src/providers/provider.rs b/packages/desktop/src/providers/provider.rs index 9f0cdacc..70d57552 100644 --- a/packages/desktop/src/providers/provider.rs +++ b/packages/desktop/src/providers/provider.rs @@ -1,10 +1,7 @@ -use std::time::Duration; - use async_trait::async_trait; -use tokio::{sync::mpsc::Sender, time}; -use tracing::error; +use tokio::sync::mpsc::Sender; -use super::{provider_ref::ProviderResult, variables::ProviderOutput}; +use super::provider_ref::ProviderResult; #[async_trait] pub trait Provider: Send + Sync { @@ -17,30 +14,36 @@ pub trait Provider: Send + Sync { } } -#[async_trait] -pub trait IntervalProvider { - /// Refresh interval in milliseconds. - fn refresh_interval_ms(&self) -> u64; - - /// Callback for when the provider is started. - async fn run_interval(&self) -> anyhow::Result; -} - -#[async_trait] -impl Provider for T { - async fn run(&self, emit_result_tx: Sender) { - let mut interval = - time::interval(Duration::from_millis(self.refresh_interval_ms())); - - loop { - interval.tick().await; - - let res = - emit_result_tx.send(self.run_interval().await.into()).await; - - if let Err(err) = res { - error!("Error sending provider result: {:?}", err); +/// Implements the `Provider` trait for the given struct. +/// +/// Expects that the struct has a `refresh_interval_ms` and `run_interval` +/// method. +#[macro_export] +macro_rules! impl_interval_provider { + ($type:ty) => { + #[async_trait::async_trait] + impl crate::providers::provider::Provider for $type { + async fn run( + &self, + emit_result_tx: tokio::sync::mpsc::Sender< + crate::providers::provider_ref::ProviderResult, + >, + ) { + let mut interval = tokio::time::interval( + std::time::Duration::from_millis(self.refresh_interval_ms()), + ); + + loop { + interval.tick().await; + + let res = + emit_result_tx.send(self.run_interval().await.into()).await; + + if let Err(err) = res { + tracing::error!("Error sending provider result: {:?}", err); + } + } } } - } + }; } diff --git a/packages/desktop/src/providers/provider_ref.rs b/packages/desktop/src/providers/provider_ref.rs index 640a88ce..02584d58 100644 --- a/packages/desktop/src/providers/provider_ref.rs +++ b/packages/desktop/src/providers/provider_ref.rs @@ -158,9 +158,7 @@ impl ProviderRef { ProviderConfig::Cpu(config) => { Box::new(CpuProvider::new(config, shared_state.sysinfo.clone())) } - ProviderConfig::Host(config) => { - Box::new(HostProvider::new(config, shared_state.sysinfo.clone())) - } + ProviderConfig::Host(config) => Box::new(HostProvider::new(config)), ProviderConfig::Ip(config) => Box::new(IpProvider::new(config)), #[cfg(windows)] ProviderConfig::Komorebi(config) => { diff --git a/packages/desktop/src/providers/weather/provider.rs b/packages/desktop/src/providers/weather/provider.rs index efc1372b..6450388f 100644 --- a/packages/desktop/src/providers/weather/provider.rs +++ b/packages/desktop/src/providers/weather/provider.rs @@ -1,16 +1,11 @@ -use std::{sync::Arc, time::Duration}; - -use async_trait::async_trait; use reqwest::Client; -use tokio::{sync::mpsc, time}; use super::{ open_meteo_res::OpenMeteoRes, WeatherOutput, WeatherProviderConfig, WeatherStatus, }; -use crate::providers::{ - provider::Provider, provider_ref::ProviderResult, - variables::ProviderOutput, +use crate::{ + impl_interval_provider, providers::variables::ProviderOutput, }; pub struct WeatherProvider { @@ -26,16 +21,18 @@ impl WeatherProvider { } } - async fn interval_output( - config: &WeatherProviderConfig, - http_client: &Client, - ) -> anyhow::Result { - let res = http_client + fn refresh_interval_ms(&self) -> u64 { + self.config.refresh_interval + } + + async fn run_interval(&self) -> anyhow::Result { + let res = self + .http_client .get("https://api.open-meteo.com/v1/forecast") .query(&[ ("temperature_unit", "celsius"), - ("latitude", &config.latitude.to_string()), - ("longitude", &config.longitude.to_string()), + ("latitude", &self.config.latitude.to_string()), + ("longitude", &self.config.longitude.to_string()), ("current_weather", "true"), ("daily", "sunset,sunrise"), ("timezone", "auto"), @@ -105,22 +102,4 @@ impl WeatherProvider { } } -#[async_trait] -impl Provider for WeatherProvider { - async fn run(&self, emit_result_tx: mpsc::Sender) { - let mut interval = - time::interval(Duration::from_millis(self.config.refresh_interval)); - - loop { - interval.tick().await; - - emit_result_tx - .send( - Self::interval_output(&self.config, &self.http_client) - .await - .into(), - ) - .await; - } - } -} +impl_interval_provider!(WeatherProvider); From bace18bb9d1699609a192615f317a91f990d9860 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Fri, 6 Sep 2024 17:31:23 +0800 Subject: [PATCH 088/138] refactor: change provider mod to use its exports --- packages/desktop/src/commands.rs | 2 +- packages/desktop/src/main.rs | 2 +- .../desktop/src/providers/battery/provider.rs | 4 +-- .../desktop/src/providers/cpu/provider.rs | 2 +- .../desktop/src/providers/host/provider.rs | 2 +- packages/desktop/src/providers/ip/provider.rs | 2 +- .../src/providers/komorebi/provider.rs | 8 ++--- .../desktop/src/providers/memory/provider.rs | 4 +-- packages/desktop/src/providers/mod.rs | 32 +++++++++++-------- .../desktop/src/providers/network/provider.rs | 2 +- packages/desktop/src/providers/provider.rs | 6 ++-- .../desktop/src/providers/provider_manager.rs | 2 +- .../desktop/src/providers/provider_ref.rs | 11 +++---- .../desktop/src/providers/weather/provider.rs | 2 +- packages/desktop/src/sys_tray.rs | 1 - 15 files changed, 40 insertions(+), 42 deletions(-) diff --git a/packages/desktop/src/commands.rs b/packages/desktop/src/commands.rs index 36ddf45c..219f761f 100644 --- a/packages/desktop/src/commands.rs +++ b/packages/desktop/src/commands.rs @@ -5,7 +5,7 @@ use tauri::{State, Window}; use crate::{ common::WindowExt, config::Config, - providers::{config::ProviderConfig, provider_manager::ProviderManager}, + providers::{ProviderConfig, ProviderManager}, window_factory::{WindowFactory, WindowState}, }; diff --git a/packages/desktop/src/main.rs b/packages/desktop/src/main.rs index e0149a70..76bc04dd 100644 --- a/packages/desktop/src/main.rs +++ b/packages/desktop/src/main.rs @@ -14,7 +14,7 @@ use crate::{ cli::{Cli, CliCommand, OutputMonitorsArgs}, config::Config, monitor_state::MonitorState, - providers::provider_manager::ProviderManager, + providers::ProviderManager, sys_tray::SysTray, window_factory::WindowFactory, }; diff --git a/packages/desktop/src/providers/battery/provider.rs b/packages/desktop/src/providers/battery/provider.rs index 71ec547b..c1df15b3 100644 --- a/packages/desktop/src/providers/battery/provider.rs +++ b/packages/desktop/src/providers/battery/provider.rs @@ -8,9 +8,7 @@ use starship_battery::{ }; use super::{BatteryOutput, BatteryProviderConfig}; -use crate::{ - impl_interval_provider, providers::variables::ProviderOutput, -}; +use crate::{impl_interval_provider, providers::ProviderOutput}; pub struct BatteryProvider { config: BatteryProviderConfig, diff --git a/packages/desktop/src/providers/cpu/provider.rs b/packages/desktop/src/providers/cpu/provider.rs index f4d62a64..2ba5d206 100644 --- a/packages/desktop/src/providers/cpu/provider.rs +++ b/packages/desktop/src/providers/cpu/provider.rs @@ -5,7 +5,7 @@ use tokio::sync::Mutex; use super::{CpuOutput, CpuProviderConfig}; use crate::{ - impl_interval_provider, providers::variables::ProviderOutput, + impl_interval_provider, providers::ProviderOutput, }; pub struct CpuProvider { diff --git a/packages/desktop/src/providers/host/provider.rs b/packages/desktop/src/providers/host/provider.rs index 03caa53f..dae9b7f3 100644 --- a/packages/desktop/src/providers/host/provider.rs +++ b/packages/desktop/src/providers/host/provider.rs @@ -2,7 +2,7 @@ use sysinfo::System; use super::{HostOutput, HostProviderConfig}; use crate::{ - impl_interval_provider, providers::variables::ProviderOutput, + impl_interval_provider, providers::ProviderOutput, }; pub struct HostProvider { diff --git a/packages/desktop/src/providers/ip/provider.rs b/packages/desktop/src/providers/ip/provider.rs index fb1c116e..61fe6d14 100644 --- a/packages/desktop/src/providers/ip/provider.rs +++ b/packages/desktop/src/providers/ip/provider.rs @@ -3,7 +3,7 @@ use reqwest::Client; use super::{ipinfo_res::IpinfoRes, IpOutput, IpProviderConfig}; use crate::{ - impl_interval_provider, providers::variables::ProviderOutput, + impl_interval_provider, providers::ProviderOutput, }; pub struct IpProvider { diff --git a/packages/desktop/src/providers/komorebi/provider.rs b/packages/desktop/src/providers/komorebi/provider.rs index adaf6cc3..2e63723b 100644 --- a/packages/desktop/src/providers/komorebi/provider.rs +++ b/packages/desktop/src/providers/komorebi/provider.rs @@ -13,12 +13,10 @@ use tracing::debug; use super::{ KomorebiContainer, KomorebiLayout, KomorebiLayoutFlip, KomorebiMonitor, - KomorebiProviderConfig, KomorebiWindow, KomorebiWorkspace, -}; -use crate::providers::{ - komorebi::KomorebiOutput, provider::Provider, - provider_ref::ProviderResult, variables::ProviderOutput, + KomorebiOutput, KomorebiProviderConfig, KomorebiWindow, + KomorebiWorkspace, }; +use crate::providers::{Provider, ProviderOutput, ProviderResult}; const SOCKET_NAME: &str = "zebar.sock"; diff --git a/packages/desktop/src/providers/memory/provider.rs b/packages/desktop/src/providers/memory/provider.rs index 14a58532..8554590b 100644 --- a/packages/desktop/src/providers/memory/provider.rs +++ b/packages/desktop/src/providers/memory/provider.rs @@ -4,9 +4,7 @@ use sysinfo::System; use tokio::sync::Mutex; use super::{MemoryOutput, MemoryProviderConfig}; -use crate::{ - impl_interval_provider, providers::variables::ProviderOutput, -}; +use crate::{impl_interval_provider, providers::ProviderOutput}; pub struct MemoryProvider { config: MemoryProviderConfig, diff --git a/packages/desktop/src/providers/mod.rs b/packages/desktop/src/providers/mod.rs index b902a359..83eba865 100644 --- a/packages/desktop/src/providers/mod.rs +++ b/packages/desktop/src/providers/mod.rs @@ -1,14 +1,20 @@ -pub mod battery; -pub mod config; -pub mod cpu; -pub mod host; -pub mod ip; +mod battery; +mod config; +mod cpu; +mod host; +mod ip; #[cfg(windows)] -pub mod komorebi; -pub mod memory; -pub mod network; -pub mod provider; -pub mod provider_manager; -pub mod provider_ref; -pub mod variables; -pub mod weather; +mod komorebi; +mod memory; +mod network; +mod provider; +mod provider_manager; +mod provider_ref; +mod variables; +mod weather; + +pub use config::*; +pub use provider::*; +pub use provider_manager::*; +pub use provider_ref::*; +pub use variables::*; diff --git a/packages/desktop/src/providers/network/provider.rs b/packages/desktop/src/providers/network/provider.rs index a1ac2827..e6385c48 100644 --- a/packages/desktop/src/providers/network/provider.rs +++ b/packages/desktop/src/providers/network/provider.rs @@ -10,7 +10,7 @@ use super::{ NetworkProviderConfig, NetworkTraffic, }; use crate::{ - impl_interval_provider, providers::variables::ProviderOutput, + impl_interval_provider, providers::ProviderOutput, }; pub struct NetworkProvider { diff --git a/packages/desktop/src/providers/provider.rs b/packages/desktop/src/providers/provider.rs index 70d57552..47887d28 100644 --- a/packages/desktop/src/providers/provider.rs +++ b/packages/desktop/src/providers/provider.rs @@ -1,7 +1,7 @@ use async_trait::async_trait; use tokio::sync::mpsc::Sender; -use super::provider_ref::ProviderResult; +use super::ProviderResult; #[async_trait] pub trait Provider: Send + Sync { @@ -22,11 +22,11 @@ pub trait Provider: Send + Sync { macro_rules! impl_interval_provider { ($type:ty) => { #[async_trait::async_trait] - impl crate::providers::provider::Provider for $type { + impl crate::providers::Provider for $type { async fn run( &self, emit_result_tx: tokio::sync::mpsc::Sender< - crate::providers::provider_ref::ProviderResult, + crate::providers::ProviderResult, >, ) { let mut interval = tokio::time::interval( diff --git a/packages/desktop/src/providers/provider_manager.rs b/packages/desktop/src/providers/provider_manager.rs index e3369d0a..2e674117 100644 --- a/packages/desktop/src/providers/provider_manager.rs +++ b/packages/desktop/src/providers/provider_manager.rs @@ -5,7 +5,7 @@ use tauri::AppHandle; use tokio::sync::Mutex; use tracing::warn; -use super::{config::ProviderConfig, provider_ref::ProviderRef}; +use super::{ProviderConfig, ProviderRef}; /// State shared between providers. #[derive(Clone)] diff --git a/packages/desktop/src/providers/provider_ref.rs b/packages/desktop/src/providers/provider_ref.rs index 02584d58..a3bca8f4 100644 --- a/packages/desktop/src/providers/provider_ref.rs +++ b/packages/desktop/src/providers/provider_ref.rs @@ -1,4 +1,4 @@ -use std::{sync::Arc, time::Instant}; +use std::time::Instant; use anyhow::bail; use serde::Serialize; @@ -10,11 +10,10 @@ use tracing::{info, warn}; #[cfg(windows)] use super::komorebi::KomorebiProvider; use super::{ - battery::BatteryProvider, config::ProviderConfig, cpu::CpuProvider, - host::HostProvider, ip::IpProvider, memory::MemoryProvider, - network::NetworkProvider, provider::Provider, - provider_manager::SharedProviderState, variables::ProviderOutput, - weather::WeatherProvider, + battery::BatteryProvider, cpu::CpuProvider, host::HostProvider, + ip::IpProvider, memory::MemoryProvider, network::NetworkProvider, + weather::WeatherProvider, Provider, ProviderConfig, ProviderOutput, + SharedProviderState, }; /// Reference to an active provider. diff --git a/packages/desktop/src/providers/weather/provider.rs b/packages/desktop/src/providers/weather/provider.rs index 6450388f..80362880 100644 --- a/packages/desktop/src/providers/weather/provider.rs +++ b/packages/desktop/src/providers/weather/provider.rs @@ -5,7 +5,7 @@ use super::{ WeatherStatus, }; use crate::{ - impl_interval_provider, providers::variables::ProviderOutput, + impl_interval_provider, providers::ProviderOutput, }; pub struct WeatherProvider { diff --git a/packages/desktop/src/sys_tray.rs b/packages/desktop/src/sys_tray.rs index 4e392ddf..e262e6e8 100644 --- a/packages/desktop/src/sys_tray.rs +++ b/packages/desktop/src/sys_tray.rs @@ -7,7 +7,6 @@ use tauri::{ tray::{TrayIcon, TrayIconBuilder}, AppHandle, Wry, }; -use tokio::task; use tracing::{error, info}; use crate::{ From de3e666c0034b571502c0253ab231650065fd3a0 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Fri, 6 Sep 2024 17:55:29 +0800 Subject: [PATCH 089/138] feat: change provider creation to be infallible --- packages/desktop/src/providers/battery/provider.rs | 13 +++---------- packages/desktop/src/providers/komorebi/provider.rs | 4 ++-- packages/desktop/src/providers/provider_manager.rs | 2 -- packages/desktop/src/providers/provider_ref.rs | 13 +++++++------ 4 files changed, 12 insertions(+), 20 deletions(-) diff --git a/packages/desktop/src/providers/battery/provider.rs b/packages/desktop/src/providers/battery/provider.rs index c1df15b3..d716a77c 100644 --- a/packages/desktop/src/providers/battery/provider.rs +++ b/packages/desktop/src/providers/battery/provider.rs @@ -12,17 +12,11 @@ use crate::{impl_interval_provider, providers::ProviderOutput}; pub struct BatteryProvider { config: BatteryProviderConfig, - battery_manager: Manager, } impl BatteryProvider { - pub fn new( - config: BatteryProviderConfig, - ) -> anyhow::Result { - Ok(BatteryProvider { - config, - battery_manager: Manager::new()?, - }) + pub fn new(config: BatteryProviderConfig) -> BatteryProvider { + BatteryProvider { config } } fn refresh_interval_ms(&self) -> u64 { @@ -30,8 +24,7 @@ impl BatteryProvider { } async fn run_interval(&self) -> anyhow::Result { - let battery = self - .battery_manager + let battery = Manager::new()? .batteries() .and_then(|mut batteries| batteries.nth(0).transpose()) .unwrap_or(None) diff --git a/packages/desktop/src/providers/komorebi/provider.rs b/packages/desktop/src/providers/komorebi/provider.rs index 2e63723b..c7364baf 100644 --- a/packages/desktop/src/providers/komorebi/provider.rs +++ b/packages/desktop/src/providers/komorebi/provider.rs @@ -21,12 +21,12 @@ use crate::providers::{Provider, ProviderOutput, ProviderResult}; const SOCKET_NAME: &str = "zebar.sock"; pub struct KomorebiProvider { - config: KomorebiProviderConfig, + _config: KomorebiProviderConfig, } impl KomorebiProvider { pub fn new(config: KomorebiProviderConfig) -> KomorebiProvider { - KomorebiProvider { config } + KomorebiProvider { _config: config } } async fn create_socket( diff --git a/packages/desktop/src/providers/provider_manager.rs b/packages/desktop/src/providers/provider_manager.rs index 2e674117..1f7069f8 100644 --- a/packages/desktop/src/providers/provider_manager.rs +++ b/packages/desktop/src/providers/provider_manager.rs @@ -18,7 +18,6 @@ pub struct SharedProviderState { pub struct ProviderManager { app_handle: AppHandle, providers: Arc>>, - // provider_cache: Arc>>, shared_state: SharedProviderState, } @@ -27,7 +26,6 @@ impl ProviderManager { Self { app_handle: app_handle.clone(), providers: Arc::new(Mutex::new(HashMap::new())), - // provider_cache: Arc::new(Mutex::new(HashMap::new())), shared_state: SharedProviderState { sysinfo: Arc::new(Mutex::new(System::new_all())), netinfo: Arc::new(Mutex::new(Networks::new_with_refreshed_list())), diff --git a/packages/desktop/src/providers/provider_ref.rs b/packages/desktop/src/providers/provider_ref.rs index a3bca8f4..dec211a5 100644 --- a/packages/desktop/src/providers/provider_ref.rs +++ b/packages/desktop/src/providers/provider_ref.rs @@ -100,7 +100,7 @@ impl ProviderRef { shared_state, emit_result_tx.clone(), stop_rx, - ); + )?; Ok(Self { config_hash, @@ -117,11 +117,10 @@ impl ProviderRef { shared_state: SharedProviderState, emit_result_tx: mpsc::Sender, mut stop_rx: mpsc::Receiver<()>, - ) { - task::spawn(async move { - // TODO: Remove unwrap. - let provider = Self::create_provider(config, shared_state).unwrap(); + ) -> anyhow::Result<()> { + let provider = Self::create_provider(config, shared_state)?; + task::spawn(async move { // TODO: Add arc `should_stop` to be passed to `run`. let run = provider.run(emit_result_tx); @@ -144,6 +143,8 @@ impl ProviderRef { info!("Provider stopped: {}", config_hash); }); + + Ok(()) } fn create_provider( @@ -152,7 +153,7 @@ impl ProviderRef { ) -> anyhow::Result> { let provider: Box = match config { ProviderConfig::Battery(config) => { - Box::new(BatteryProvider::new(config)?) + Box::new(BatteryProvider::new(config)) } ProviderConfig::Cpu(config) => { Box::new(CpuProvider::new(config, shared_state.sysinfo.clone())) From 7aa9b98208171446400f027aa803455f1264adf0 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Fri, 6 Sep 2024 18:46:12 +0800 Subject: [PATCH 090/138] feat: add output caching to provider ref --- examples/solidjs-ts/src/index.tsx | 18 +-- .../desktop/src/providers/provider_ref.rs | 120 +++++++++--------- 2 files changed, 71 insertions(+), 67 deletions(-) diff --git a/examples/solidjs-ts/src/index.tsx b/examples/solidjs-ts/src/index.tsx index 0b6f6bd5..c19cd189 100644 --- a/examples/solidjs-ts/src/index.tsx +++ b/examples/solidjs-ts/src/index.tsx @@ -6,11 +6,11 @@ import { init } from 'zebar'; import { createEffect } from 'solid-js'; const zebarCtx = await init(); -const [cpu, battery] = await Promise.all([ +const [cpu, battery, memory, weather] = await Promise.all([ zebarCtx.createProvider({ type: 'cpu' }), zebarCtx.createProvider({ type: 'battery' }), - // zebarCtx.createProvider({ type: 'memory' }), - // zebarCtx.createProvider({ type: 'weather' }), + zebarCtx.createProvider({ type: 'memory' }), + zebarCtx.createProvider({ type: 'weather' }), ]); render(() => , document.getElementById('root')!); @@ -19,22 +19,22 @@ function App() { const [store, setStore] = createStore({ cpu: cpu.output, battery: battery.output, - // memory: memory.output, - // weather: weather.output, + memory: memory.output, + weather: weather.output, }); cpu.onOutput(cpu => setStore({ cpu })); battery.onOutput(battery => setStore({ battery })); - // memory.onOutput(memory => setStore({ memory })); - // weather.onOutput(weather => setStore({ weather })); + memory.onOutput(memory => setStore({ memory })); + weather.onOutput(weather => setStore({ weather })); return (
cpu: {store.cpu.usage} battery: {store.battery?.chargePercent} - {/* memory: {store.memory.usage} + memory: {store.memory.usage} weather temp: {store.weather.celsiusTemp} - weather status: {store.weather.status} */} + weather status: {store.weather.status}
); } diff --git a/packages/desktop/src/providers/provider_ref.rs b/packages/desktop/src/providers/provider_ref.rs index dec211a5..105ce2d0 100644 --- a/packages/desktop/src/providers/provider_ref.rs +++ b/packages/desktop/src/providers/provider_ref.rs @@ -1,10 +1,13 @@ -use std::time::Instant; +use std::sync::Arc; use anyhow::bail; use serde::Serialize; use serde_json::json; use tauri::{AppHandle, Emitter}; -use tokio::{sync::mpsc, task}; +use tokio::{ + sync::{mpsc, Mutex}, + task, +}; use tracing::{info, warn}; #[cfg(windows)] @@ -18,17 +21,15 @@ use super::{ /// Reference to an active provider. pub struct ProviderRef { - config_hash: String, - cache: Option, + /// Cache for provider output. + cache: Arc>>>, + + /// Sender channel for emitting provider output/error to frontend + /// clients. emit_result_tx: mpsc::Sender, - stop_tx: mpsc::Sender<()>, -} -/// Cache for provider output. -#[derive(Debug, Clone)] -pub struct ProviderCache { - timestamp: Instant, - output: Box, + /// Sender channel for stopping the provider. + stop_tx: mpsc::Sender<()>, } /// Provider output/error emitted to frontend clients. @@ -60,18 +61,47 @@ impl ProviderRef { config_hash: String, shared_state: SharedProviderState, ) -> anyhow::Result { - let (emit_result_tx, mut emit_result_rx) = + let cache = Arc::new(Mutex::new(None)); + + let (stop_tx, stop_rx) = mpsc::channel::<()>(1); + let (emit_result_tx, emit_result_rx) = mpsc::channel::(1); - let config_hash_clone = config_hash.clone(); - let app_handle = app_handle.clone(); + Self::start_output_listener( + app_handle.clone(), + config_hash.clone(), + cache.clone(), + emit_result_rx, + ); + + Self::start_provider( + config, + config_hash, + shared_state, + emit_result_tx.clone(), + stop_rx, + )?; + + Ok(Self { + cache, + emit_result_tx, + stop_tx, + }) + } + + fn start_output_listener( + app_handle: AppHandle, + config_hash: String, + cache: Arc>>>, + mut emit_result_rx: mpsc::Receiver, + ) { task::spawn(async move { while let Some(output) = emit_result_rx.recv().await { - info!("Emitting for provider: {}", config_hash_clone); + info!("Emitting for provider: {}", config_hash); let output = Box::new(output); let payload = json!({ - "configHash": config_hash_clone.clone(), + "configHash": config_hash.clone(), "result": *output.clone(), }); @@ -80,34 +110,13 @@ impl ProviderRef { } // Update the provider's output cache. - // if let Ok(mut providers) = providers.try_lock() { - // if let Some(found_provider) = - // providers.get_mut(&output.config_hash) - // { - // found_provider.update_cache(output); - // } - // } else { - // warn!("Failed to update provider output cache."); - // } + if let Ok(mut providers) = cache.try_lock() { + *providers = Some(output); + } else { + warn!("Failed to update provider output cache."); + } } }); - - let (stop_tx, stop_rx) = mpsc::channel::<()>(1); - - Self::start_provider( - config, - config_hash.clone(), - shared_state, - emit_result_tx.clone(), - stop_rx, - )?; - - Ok(Self { - config_hash, - cache: None, - emit_result_tx, - stop_tx, - }) } /// Starts the provider in a separate task. @@ -181,22 +190,16 @@ impl ProviderRef { Ok(provider) } - /// Updates cache with the given output. - pub fn update_cache(&mut self, output: Box) { - self.cache = Some(ProviderCache { - timestamp: Instant::now(), - output, - }); - } - - /// Refreshes the provider. + /// Re-emits the latest provider output. /// - /// Since the previous output of providers is cached, if within the - /// minimum refresh interval, send the previous output. - pub async fn refresh(&mut self) -> anyhow::Result<()> { - // if let Some(cache) = self.cache { - // self.emit_result_tx.send(*cache.output.clone()).await?; - // } + /// No-ops if the provider hasn't outputted yet, since the provider will + /// anyways emit its output after initialization. + pub async fn refresh(&self) -> anyhow::Result<()> { + let cache = { self.cache.lock().await.clone() }; + + if let Some(cache) = cache { + self.emit_result_tx.send(*cache).await?; + } Ok(()) } @@ -204,8 +207,9 @@ impl ProviderRef { /// Stops the given provider. /// /// This triggers any necessary cleanup. - pub async fn stop(&mut self) -> anyhow::Result<()> { + pub async fn stop(&self) -> anyhow::Result<()> { self.stop_tx.send(()).await?; + Ok(()) } } From 17ae91fc2d1966e3477cd257c79a18de65859d8f Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Sat, 7 Sep 2024 15:56:45 +0800 Subject: [PATCH 091/138] feat: update system tray on window open/close; launch window on enable --- packages/desktop/src/sys_tray.rs | 53 ++++++++++++++++----- packages/desktop/src/window_factory.rs | 66 +++++++++++++++++++++----- 2 files changed, 95 insertions(+), 24 deletions(-) diff --git a/packages/desktop/src/sys_tray.rs b/packages/desktop/src/sys_tray.rs index e262e6e8..c4deab29 100644 --- a/packages/desktop/src/sys_tray.rs +++ b/packages/desktop/src/sys_tray.rs @@ -7,6 +7,7 @@ use tauri::{ tray::{TrayIcon, TrayIconBuilder}, AppHandle, Wry, }; +use tokio::task; use tracing::{error, info}; use crate::{ @@ -113,19 +114,36 @@ impl SysTray { fn start_listener(self: Arc) { let mut config_changes_rx = self.config.changes_tx.subscribe(); - let mut window_changes_rx = self.window_factory.changes_tx.subscribe(); + let mut window_open_rx = self.window_factory.open_tx.subscribe(); + let mut window_close_rx = self.window_factory.close_tx.subscribe(); tokio::spawn(async move { loop { - tokio::select! { - Ok(_) = config_changes_rx.recv() => { - println!("Config changes received."); - if let Some(tray_icon) = self.tray_icon.as_ref() { - tray_icon.set_menu(Some(self.create_tray_menu().await.unwrap())); - } - } - Ok(_) = window_changes_rx.recv() => { - println!("Window changes received."); + let event = tokio::select! { + Ok(_) = config_changes_rx.recv() => "Config changes", + Ok(_) = window_open_rx.recv() => "Window open", + Ok(_) = window_close_rx.recv() => "Window close", + }; + + info!("{} received in system tray. Updating menu.", event); + + // Drain receiver channels if multiple events are queued up. + config_changes_rx = config_changes_rx.resubscribe(); + window_open_rx = window_open_rx.resubscribe(); + window_close_rx = window_close_rx.resubscribe(); + + if let Some(tray_icon) = self.tray_icon.as_ref() { + if let Err(err) = + self.create_tray_menu().await.and_then(|menu| { + tray_icon + .set_menu(Some(menu)) + .context("Failed to set tray menu.") + }) + { + error!( + "Failed to update tray menu after {} event: {:?}", + event, err + ); } } } @@ -197,9 +215,18 @@ impl SysTray { MenuEvent::EnableWindowConfig(path) => { info!("Window config at path {} enabled.", path); - // task::spawn(async move { - // window_factory.open(path).await; - // }) + task::spawn(async move { + let window_config = config + .window_config_by_path(&config.join_path(&path)) + .await + .unwrap() + .with_context(|| { + format!("Window config not found at {}.", path) + }) + .unwrap(); + + window_factory.open(window_config).await; + }); } MenuEvent::StartupWindowConfig(path) => { info!("Window config at path {} set to launch on startup.", path); diff --git a/packages/desktop/src/window_factory.rs b/packages/desktop/src/window_factory.rs index 688cf576..5e12e414 100644 --- a/packages/desktop/src/window_factory.rs +++ b/packages/desktop/src/window_factory.rs @@ -7,9 +7,12 @@ use std::{ }; use serde::Serialize; -use tauri::{AppHandle, WebviewUrl, WebviewWindowBuilder}; -use tokio::sync::{broadcast, Mutex}; -use tracing::info; +use tauri::{AppHandle, WebviewUrl, WebviewWindowBuilder, WindowEvent}; +use tokio::{ + sync::{broadcast, Mutex}, + task, +}; +use tracing::{error, info}; use crate::{ common::WindowExt, @@ -22,9 +25,13 @@ pub struct WindowFactory { /// Handle to the Tauri application. app_handle: AppHandle, - _changes_rx: broadcast::Receiver>, + _close_rx: broadcast::Receiver, + + pub close_tx: broadcast::Sender, + + _open_rx: broadcast::Receiver, - pub changes_tx: broadcast::Sender>, + pub open_tx: broadcast::Sender, /// Reference to `MonitorState` for window positioning. monitor_state: Arc, @@ -69,12 +76,15 @@ impl WindowFactory { app_handle: &AppHandle, monitor_state: Arc, ) -> Self { - let (changes_tx, _changes_rx) = broadcast::channel(16); + let (open_tx, _open_rx) = broadcast::channel(16); + let (close_tx, _close_rx) = broadcast::channel(16); Self { app_handle: app_handle.clone(), - _changes_rx, - changes_tx, + _close_rx, + close_tx, + _open_rx, + open_tx, monitor_state, window_count: Arc::new(AtomicU32::new(0)), window_states: Arc::new(Mutex::new(HashMap::new())), @@ -93,15 +103,17 @@ impl WindowFactory { } = &config_entry; for placement in self.window_placements(config) { + // Use running window count as a unique ID for the window. let new_count = self.window_count.fetch_add(1, Ordering::Relaxed) + 1; + let window_id = new_count.to_string(); info!("Creating window #{} from {}", new_count, config_path); // Note that window label needs to be globally unique. let window = WebviewWindowBuilder::new( &self.app_handle, - new_count.to_string(), + window_id.clone(), WebviewUrl::App( format!("http://asset.localhost/{}", html_path).into(), ), @@ -119,7 +131,7 @@ impl WindowFactory { .build()?; let state = WindowState { - window_id: new_count.to_string(), + window_id: window_id.clone(), config: config.clone(), config_path: config_path.clone(), html_path: html_path.clone(), @@ -139,12 +151,44 @@ impl WindowFactory { .set_tool_window(!config.launch_options.shown_in_taskbar); let mut window_states = self.window_states.lock().await; - window_states.insert(state.window_id.clone(), state); + window_states.insert(state.window_id.clone(), state.clone()); + + self.register_window_events(&window, window_id); + self.open_tx.send(state)?; } Ok(()) } + /// Registers window events for a given window. + fn register_window_events( + &self, + window: &tauri::WebviewWindow, + window_id: String, + ) { + let window_states = self.window_states.clone(); + let close_tx = self.close_tx.clone(); + + window.on_window_event(move |event| { + if let WindowEvent::Destroyed = event { + let window_states = window_states.clone(); + let close_tx = close_tx.clone(); + let window_id = window_id.clone(); + + task::spawn(async move { + let mut window_states = window_states.lock().await; + + // Remove the window state and broadcast the close event. + if let Some(state) = window_states.remove(&window_id) { + if let Err(err) = close_tx.send(state) { + error!("Failed to send window close event: {:?}", err); + } + } + }); + } + }); + } + /// Returns coordinates for window placement based on the given config. fn window_placements( &self, From 00806a6c1404753c2542812a7a6dc2860065d1bb Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Sat, 7 Sep 2024 16:15:10 +0800 Subject: [PATCH 092/138] refactor: change systray menu handling to be async --- packages/desktop/src/sys_tray.rs | 55 +++++++++++++++++--------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/packages/desktop/src/sys_tray.rs b/packages/desktop/src/sys_tray.rs index c4deab29..6fa0157d 100644 --- a/packages/desktop/src/sys_tray.rs +++ b/packages/desktop/src/sys_tray.rs @@ -92,20 +92,29 @@ impl SysTray { .menu(&self.create_tray_menu().await?) .tooltip(tooltip) .on_menu_event(move |app, event| { - let event = MenuEvent::from_str(event.id.as_ref()); - - let event_res = event.map(|event| { - Self::handle_menu_event( - event, - &app, - config.clone(), - window_factory.clone(), - ) - }); + let app_handle = app.clone(); + let config = config.clone(); + let window_factory = window_factory.clone(); - if let Err(err) = event_res { - error!("{:?}", err); - } + task::spawn(async move { + let event = MenuEvent::from_str(event.id.as_ref()); + + if let Ok(event) = event { + info!("Received tray menu event: {}", event.to_string()); + + let res = Self::handle_menu_event( + event, + app_handle, + config, + window_factory, + ) + .await; + + if let Err(err) = res { + error!("{:?}", err); + } + } + }); }) .build(&self.app_handle)?; @@ -193,9 +202,9 @@ impl SysTray { } /// Callback for system tray menu events. - fn handle_menu_event( + async fn handle_menu_event( event: MenuEvent, - app_handle: &AppHandle, + app_handle: AppHandle, config: Arc, window_factory: Arc, ) -> anyhow::Result<()> { @@ -215,18 +224,12 @@ impl SysTray { MenuEvent::EnableWindowConfig(path) => { info!("Window config at path {} enabled.", path); - task::spawn(async move { - let window_config = config - .window_config_by_path(&config.join_path(&path)) - .await - .unwrap() - .with_context(|| { - format!("Window config not found at {}.", path) - }) - .unwrap(); + let window_config = config + .window_config_by_path(&path) + .await? + .context("Window config not found.")?; - window_factory.open(window_config).await; - }); + window_factory.open(window_config).await?; } MenuEvent::StartupWindowConfig(path) => { info!("Window config at path {} set to launch on startup.", path); From 6580e930b8dbf69066f542bd4417836433e736fd Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Sat, 7 Sep 2024 16:35:11 +0800 Subject: [PATCH 093/138] refactor: add `to_unicode_string` path ext --- packages/desktop/src/common/path_ext.rs | 11 +++++++++- packages/desktop/src/config.rs | 6 +---- packages/desktop/src/sys_tray.rs | 29 +++++++++++-------------- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/packages/desktop/src/common/path_ext.rs b/packages/desktop/src/common/path_ext.rs index 5c5731cc..f07b8945 100644 --- a/packages/desktop/src/common/path_ext.rs +++ b/packages/desktop/src/common/path_ext.rs @@ -16,12 +16,17 @@ where /// path.canonicalize_pretty().unwrap(); // "C:\\Users\\John\\Desktop\\test" /// ``` fn canonicalize_pretty(&self) -> anyhow::Result; + + /// Converts the path to a unicode string. + /// + /// Short-hand for `.to_string_lossy().to_string()`. + fn to_unicode_string(&self) -> String; } impl PathExt for PathBuf { fn canonicalize_pretty(&self) -> anyhow::Result { let canonicalized = fs::canonicalize(self)?; - let canonicalized_str = canonicalized.to_string_lossy().to_string(); + let canonicalized_str = canonicalized.to_unicode_string(); #[cfg(not(windows))] { @@ -49,4 +54,8 @@ impl PathExt for PathBuf { Ok(stripped_str) } } + + fn to_unicode_string(&self) -> String { + self.to_string_lossy().to_string() + } } diff --git a/packages/desktop/src/config.rs b/packages/desktop/src/config.rs index bc50ce64..edfb21f2 100644 --- a/packages/desktop/src/config.rs +++ b/packages/desktop/src/config.rs @@ -320,11 +320,7 @@ impl Config { /// /// Returns an absolute path. pub fn join_path(&self, config_path: &str) -> String { - self - .config_dir - .join(config_path) - .to_string_lossy() - .to_string() + self.config_dir.join(config_path).to_unicode_string() } /// Returns the window config at the given absolute path. diff --git a/packages/desktop/src/sys_tray.rs b/packages/desktop/src/sys_tray.rs index 6fa0157d..cf492841 100644 --- a/packages/desktop/src/sys_tray.rs +++ b/packages/desktop/src/sys_tray.rs @@ -11,6 +11,7 @@ use tokio::task; use tracing::{error, info}; use crate::{ + common::PathExt, config::Config, window_factory::{WindowFactory, WindowState}, }; @@ -214,12 +215,13 @@ impl SysTray { config .open_config_dir() - .context("Failed to open config folder.")?; + .context("Failed to open config folder.") } MenuEvent::Exit => { info!("Exiting through system tray."); - app_handle.exit(0) + app_handle.exit(0); + Ok(()) } MenuEvent::EnableWindowConfig(path) => { info!("Window config at path {} enabled.", path); @@ -229,18 +231,15 @@ impl SysTray { .await? .context("Window config not found.")?; - window_factory.open(window_config).await?; + window_factory.open(window_config).await } MenuEvent::StartupWindowConfig(path) => { info!("Window config at path {} set to launch on startup.", path); - // task::spawn(async move { - // config.add_startup_config(path).await; - // }) + // config.add_startup_config(path).await + Ok(()) } - }; - - Ok(()) + } } /// Creates and returns a submenu for the window configs. @@ -311,12 +310,10 @@ impl SysTray { config_path: &str, config_dir: &PathBuf, ) -> String { - let path = PathBuf::from(config_path) - .strip_prefix(config_dir) - .unwrap_or(&PathBuf::from(config_path)) - .to_string_lossy() - .to_string(); - - path.strip_suffix(".zebar.json").unwrap_or(&path).into() + config_path + .strip_prefix(&config_dir.to_unicode_string()) + .and_then(|path| path.strip_suffix(".zebar.json")) + .unwrap_or(config_path) + .into() } } From 607fd12fe1da0220adfbec8f0a934c3aa0d249b0 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Sat, 7 Sep 2024 18:40:57 +0800 Subject: [PATCH 094/138] feat: add `strip_config_dir`; rename `join_path` -> `join_config_dir` --- packages/desktop/src/config.rs | 16 ++++++++++++++-- packages/desktop/src/main.rs | 2 +- packages/desktop/src/sys_tray.rs | 15 +++++++-------- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/packages/desktop/src/config.rs b/packages/desktop/src/config.rs index edfb21f2..699149c0 100644 --- a/packages/desktop/src/config.rs +++ b/packages/desktop/src/config.rs @@ -305,7 +305,7 @@ impl Config { for config_path in startup_configs { let config = self - .window_config_by_path(&self.join_path(&config_path)) + .window_config_by_path(&self.join_config_dir(&config_path)) .await .unwrap_or(None) .context("Failed to get window config.")?; @@ -319,10 +319,22 @@ impl Config { /// Joins the given path with the config directory path. /// /// Returns an absolute path. - pub fn join_path(&self, config_path: &str) -> String { + pub fn join_config_dir(&self, config_path: &str) -> String { self.config_dir.join(config_path).to_unicode_string() } + /// Strips the config directory path from the given path. + /// + /// Returns a relative path. + pub fn strip_config_dir<'a>( + &self, + config_path: &'a str, + ) -> anyhow::Result<&'a str> { + config_path + .strip_prefix(&self.config_dir.to_unicode_string()) + .context("Failed to strip config directory path.") + } + /// Returns the window config at the given absolute path. pub async fn window_config_by_path( &self, diff --git a/packages/desktop/src/main.rs b/packages/desktop/src/main.rs index 76bc04dd..68accd11 100644 --- a/packages/desktop/src/main.rs +++ b/packages/desktop/src/main.rs @@ -206,7 +206,7 @@ async fn open_windows_by_cli_command( let window_configs = match cli.command() { CliCommand::Open(args) => { let window_config = config - .window_config_by_path(&config.join_path(&args.config_path)) + .window_config_by_path(&config.join_config_dir(&args.config_path)) .await? .with_context(|| { format!("Window config not found at {}.", args.config_path) diff --git a/packages/desktop/src/sys_tray.rs b/packages/desktop/src/sys_tray.rs index cf492841..6f2ae904 100644 --- a/packages/desktop/src/sys_tray.rs +++ b/packages/desktop/src/sys_tray.rs @@ -184,7 +184,7 @@ impl SysTray { let label = format!( "({}) {}", states.len(), - Self::format_config_path(&config_path, &self.config.config_dir) + Self::format_config_path(&self.config, &config_path) ); tray_menu = tray_menu.item(&self.create_config_menu( @@ -252,10 +252,8 @@ impl SysTray { // Add each window config to the menu. for window_config in &self.config.window_configs().await { - let label = Self::format_config_path( - &window_config.config_path, - &self.config.config_dir, - ); + let label = + Self::format_config_path(&self.config, &window_config.config_path); let menu_item = self.create_config_menu( &window_config.config_path, @@ -307,11 +305,12 @@ impl SysTray { /// Formats the config path for display in the system tray. fn format_config_path( + config: &Arc, config_path: &str, - config_dir: &PathBuf, ) -> String { - config_path - .strip_prefix(&config_dir.to_unicode_string()) + config + .strip_config_dir(config_path) + .ok() .and_then(|path| path.strip_suffix(".zebar.json")) .unwrap_or(config_path) .into() From 8b40dda5b136aa3ff7113bba24559dfd469543b0 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Sat, 7 Sep 2024 21:29:20 +0800 Subject: [PATCH 095/138] feat: progress on implementing sys tray menu toggles --- packages/desktop/src/config.rs | 42 +++++++++++ packages/desktop/src/sys_tray.rs | 97 ++++++++++++++++---------- packages/desktop/src/window_factory.rs | 35 +++++++++- 3 files changed, 137 insertions(+), 37 deletions(-) diff --git a/packages/desktop/src/config.rs b/packages/desktop/src/config.rs index 699149c0..0956faf9 100644 --- a/packages/desktop/src/config.rs +++ b/packages/desktop/src/config.rs @@ -229,6 +229,18 @@ impl Config { } } + /// Writes to the global settings file. + async fn write_settings( + &self, + settings: &SettingsConfig, + ) -> anyhow::Result<()> { + let settings_path = self.config_dir.join("settings.json"); + + fs::write(&settings_path, serde_json::to_string_pretty(settings)?)?; + + Ok(()) + } + /// Recursively aggregates all valid window configs in the given /// directory. /// @@ -316,6 +328,36 @@ impl Config { Ok(result) } + /// Adds the given config to be launched on startup. + /// + /// Config path must be absolute. + pub async fn add_startup_config( + &self, + config_path: &str, + ) -> anyhow::Result<()> { + let startup_configs = self.startup_window_configs().await?; + + // Check if the config is already set to be launched on startup. + if startup_configs + .iter() + .find(|config| config.config_path == config_path) + .is_some() + { + return Ok(()); + } + + let relative_path = self.strip_config_dir(config_path)?; + + let mut settings = self.settings.lock().await; + let mut new_settings = settings.clone(); + new_settings.startup_configs.push(relative_path.to_string()); + + self.write_settings(&new_settings).await?; + *settings = new_settings; + + Ok(()) + } + /// Joins the given path with the config directory path. /// /// Returns an absolute path. diff --git a/packages/desktop/src/sys_tray.rs b/packages/desktop/src/sys_tray.rs index 6f2ae904..d543dcb1 100644 --- a/packages/desktop/src/sys_tray.rs +++ b/packages/desktop/src/sys_tray.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, path::PathBuf, str::FromStr, sync::Arc}; +use std::{collections::HashMap, str::FromStr, sync::Arc}; use anyhow::{bail, Context}; use tauri::{ @@ -11,7 +11,6 @@ use tokio::task; use tracing::{error, info}; use crate::{ - common::PathExt, config::Config, window_factory::{WindowFactory, WindowState}, }; @@ -20,8 +19,8 @@ use crate::{ enum MenuEvent { ShowConfigFolder, Exit, - EnableWindowConfig(String), - StartupWindowConfig(String), + ToggleWindowConfig { enable: bool, path: String }, + ToggleStartupWindowConfig { enable: bool, path: String }, } impl ToString for MenuEvent { @@ -29,8 +28,12 @@ impl ToString for MenuEvent { match self { MenuEvent::ShowConfigFolder => "show_config_folder".to_string(), MenuEvent::Exit => "exit".to_string(), - MenuEvent::EnableWindowConfig(id) => format!("enable_{}", id), - MenuEvent::StartupWindowConfig(id) => format!("startup_{}", id), + MenuEvent::ToggleWindowConfig { enable, path } => { + format!("toggle_window_config_{}_{}", enable, path) + } + MenuEvent::ToggleStartupWindowConfig { enable, path } => { + format!("toggle_startup_window_config_{}_{}", enable, path) + } } } } @@ -39,16 +42,24 @@ impl FromStr for MenuEvent { type Err = anyhow::Error; fn from_str(event: &str) -> Result { - if event == "show_config_folder" { - Ok(MenuEvent::ShowConfigFolder) - } else if event == "exit" { - Ok(MenuEvent::Exit) - } else if let Some(id) = event.strip_prefix("enable_") { - Ok(MenuEvent::EnableWindowConfig(id.to_string())) - } else if let Some(id) = event.strip_prefix("startup_") { - Ok(MenuEvent::StartupWindowConfig(id.to_string())) - } else { - bail!("Invalid menu event: {}", event) + let parts: Vec<&str> = event.split('_').collect(); + + match parts.as_slice() { + ["show", "config", "folder"] => Ok(Self::ShowConfigFolder), + ["exit"] => Ok(Self::Exit), + ["toggle", "window", "config", enable @ ("true" | "false"), path @ ..] => { + Ok(Self::ToggleWindowConfig { + enable: *enable == "true", + path: path.join("_"), + }) + } + ["toggle", "startup", "window", "config", enable @ ("true" | "false"), path @ ..] => { + Ok(Self::ToggleStartupWindowConfig { + enable: *enable == "true", + path: path.join("_"), + }) + } + _ => bail!("Invalid menu event: {}", event), } } } @@ -190,7 +201,9 @@ impl SysTray { tray_menu = tray_menu.item(&self.create_config_menu( &config_path, &label, - &states, + !states.is_empty(), + // TODO: Get whether it's launched on startup. + false, )?); } @@ -223,18 +236,25 @@ impl SysTray { app_handle.exit(0); Ok(()) } - MenuEvent::EnableWindowConfig(path) => { - info!("Window config at path {} enabled.", path); + MenuEvent::ToggleWindowConfig { enable, path } => { + info!( + "Window config toggled {} at path {} enabled.", + enable, path + ); - let window_config = config - .window_config_by_path(&path) - .await? - .context("Window config not found.")?; + // let window_config = config + // .window_config_by_path(&path) + // .await? + // .context("Window config not found.")?; - window_factory.open(window_config).await + // window_factory.open(window_config).await + Ok(()) } - MenuEvent::StartupWindowConfig(path) => { - info!("Window config at path {} set to launch on startup.", path); + MenuEvent::ToggleStartupWindowConfig { enable, path } => { + info!( + "Window config toggled {} at path {} set to launch on startup.", + enable, path + ); // config.add_startup_config(path).await Ok(()) @@ -258,9 +278,9 @@ impl SysTray { let menu_item = self.create_config_menu( &window_config.config_path, &label, - window_states - .get(&window_config.config_path) - .unwrap_or(&vec![]), + window_states.contains_key(&window_config.config_path), + // TODO: Get whether it's launched on startup. + false, )?; configs_menu = configs_menu.item(&menu_item); @@ -274,25 +294,30 @@ impl SysTray { &self, config_path: &str, label: &str, - window_states: &Vec, + is_enabled: bool, + is_launched_on_startup: bool, ) -> anyhow::Result> { let enabled_item = CheckMenuItem::with_id( &self.app_handle, - MenuEvent::EnableWindowConfig(config_path.to_string()), + MenuEvent::ToggleWindowConfig { + enable: is_enabled, + path: config_path.to_string(), + }, "Enabled", true, - !window_states.is_empty(), + is_enabled, None::<&str>, )?; - // ** - // TODO: Get whether it's launched on startup. let startup_item = CheckMenuItem::with_id( &self.app_handle, - MenuEvent::StartupWindowConfig(config_path.to_string()), + MenuEvent::ToggleStartupWindowConfig { + enable: is_launched_on_startup, + path: config_path.to_string(), + }, "Launch on startup", true, - true, + is_launched_on_startup, None::<&str>, )?; diff --git a/packages/desktop/src/window_factory.rs b/packages/desktop/src/window_factory.rs index 5e12e414..cc3bd7e3 100644 --- a/packages/desktop/src/window_factory.rs +++ b/packages/desktop/src/window_factory.rs @@ -6,8 +6,11 @@ use std::{ }, }; +use anyhow::Context; use serde::Serialize; -use tauri::{AppHandle, WebviewUrl, WebviewWindowBuilder, WindowEvent}; +use tauri::{ + AppHandle, Manager, WebviewUrl, WebviewWindowBuilder, WindowEvent, +}; use tokio::{ sync::{broadcast, Mutex}, task, @@ -250,6 +253,36 @@ impl WindowFactory { placements } + /// Closes a single window by a given window ID. + pub fn close_by_id(&self, window_id: &str) -> anyhow::Result<()> { + let window = self + .app_handle + .get_webview_window(window_id) + .context("No window found with the given ID.")?; + + window.close()?; + + Ok(()) + } + + /// Closes all windows with the given config path. + pub async fn close_by_path( + &self, + config_path: &str, + ) -> anyhow::Result<()> { + let window_states = self.states_by_config_path().await; + + let found_window_states = window_states + .get(config_path) + .context("No windows found with the given config path.")?; + + for window_state in found_window_states { + self.close_by_id(&window_state.window_id)?; + } + + Ok(()) + } + /// Returns window state by a given window ID. pub async fn state_by_id(&self, window_id: &str) -> Option { self.window_states.lock().await.get(window_id).cloned() From d11bd69614ebf13be78b719c1a7018be396d6ca2 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Sun, 8 Sep 2024 03:16:12 +0800 Subject: [PATCH 096/138] feat: conditionally toggle system tray menu enable + launch on startup --- packages/desktop/src/config.rs | 56 ++++++++++++++++++++-------- packages/desktop/src/sys_tray.rs | 64 ++++++++++++++++++++------------ 2 files changed, 80 insertions(+), 40 deletions(-) diff --git a/packages/desktop/src/config.rs b/packages/desktop/src/config.rs index 0956faf9..89fff0b9 100644 --- a/packages/desktop/src/config.rs +++ b/packages/desktop/src/config.rs @@ -121,11 +121,9 @@ pub struct Config { /// List of window configs. pub window_configs: Arc>>, - _changes_rx: - broadcast::Receiver<(SettingsConfig, Vec)>, + _settings_change_rx: broadcast::Receiver, - pub changes_tx: - broadcast::Sender<(SettingsConfig, Vec)>, + pub settings_change_tx: broadcast::Sender, } #[derive(Clone, Debug)] @@ -160,15 +158,15 @@ impl Config { let settings = Self::read_settings_or_init(app_handle, &config_dir)?; let window_configs = Self::read_window_configs(&config_dir)?; - let (changes_tx, _changes_rx) = broadcast::channel(16); + let (settings_change_tx, _settings_change_rx) = broadcast::channel(16); Ok(Self { app_handle: app_handle.clone(), config_dir, settings: Arc::new(Mutex::new(settings)), window_configs: Arc::new(Mutex::new(window_configs)), - _changes_rx, - changes_tx, + _settings_change_rx, + settings_change_tx, }) } @@ -188,7 +186,7 @@ impl Config { *window_configs = new_window_configs.clone(); } - self.changes_tx.send((new_settings, new_window_configs))?; + self.settings_change_tx.send(new_settings)?; Ok(()) } @@ -232,11 +230,19 @@ impl Config { /// Writes to the global settings file. async fn write_settings( &self, - settings: &SettingsConfig, + new_settings: SettingsConfig, ) -> anyhow::Result<()> { let settings_path = self.config_dir.join("settings.json"); - fs::write(&settings_path, serde_json::to_string_pretty(settings)?)?; + fs::write( + &settings_path, + serde_json::to_string_pretty(&new_settings)?, + )?; + + let mut settings = self.settings.lock().await; + *settings = new_settings.clone(); + + self.settings_change_tx.send(new_settings)?; Ok(()) } @@ -346,14 +352,32 @@ impl Config { return Ok(()); } - let relative_path = self.strip_config_dir(config_path)?; + // Add the path to startup configs. + let mut new_settings = { self.settings.lock().await.clone() }; + new_settings + .startup_configs + .push(self.strip_config_dir(config_path)?.to_string()); - let mut settings = self.settings.lock().await; - let mut new_settings = settings.clone(); - new_settings.startup_configs.push(relative_path.to_string()); + self.write_settings(new_settings).await?; + + Ok(()) + } + + /// Removes the given config from being launched on startup. + /// + /// Config path must be absolute. + pub async fn remove_startup_config( + &self, + config_path: &str, + ) -> anyhow::Result<()> { + let rel_path = self.strip_config_dir(config_path)?.to_string(); + + let mut new_settings = { self.settings.lock().await.clone() }; + new_settings + .startup_configs + .retain(|path| path != &rel_path); - self.write_settings(&new_settings).await?; - *settings = new_settings; + self.write_settings(new_settings).await?; Ok(()) } diff --git a/packages/desktop/src/sys_tray.rs b/packages/desktop/src/sys_tray.rs index d543dcb1..c9710d43 100644 --- a/packages/desktop/src/sys_tray.rs +++ b/packages/desktop/src/sys_tray.rs @@ -134,24 +134,25 @@ impl SysTray { } fn start_listener(self: Arc) { - let mut config_changes_rx = self.config.changes_tx.subscribe(); let mut window_open_rx = self.window_factory.open_tx.subscribe(); let mut window_close_rx = self.window_factory.close_tx.subscribe(); + let mut settings_change_rx = + self.config.settings_change_tx.subscribe(); tokio::spawn(async move { loop { let event = tokio::select! { - Ok(_) = config_changes_rx.recv() => "Config changes", Ok(_) = window_open_rx.recv() => "Window open", Ok(_) = window_close_rx.recv() => "Window close", + Ok(_) = settings_change_rx.recv() => "Settings change", }; info!("{} received in system tray. Updating menu.", event); // Drain receiver channels if multiple events are queued up. - config_changes_rx = config_changes_rx.resubscribe(); window_open_rx = window_open_rx.resubscribe(); window_close_rx = window_close_rx.resubscribe(); + settings_change_rx = settings_change_rx.resubscribe(); if let Some(tray_icon) = self.tray_icon.as_ref() { if let Err(err) = @@ -184,8 +185,20 @@ impl SysTray { async fn create_tray_menu(&self) -> anyhow::Result> { let window_states = self.window_factory.states_by_config_path().await; + let startup_config_paths = self + .config + .startup_window_configs() + .await? + .into_iter() + .map(|entry| entry.config_path) + .collect(); + + let configs_menu = self + .create_configs_menu(&window_states, &startup_config_paths) + .await?; + let mut tray_menu = MenuBuilder::new(&self.app_handle) - .item(&self.create_configs_menu(&window_states).await?) + .item(&configs_menu) .text(MenuEvent::ShowConfigFolder, "Show config folder") .separator(); @@ -202,8 +215,7 @@ impl SysTray { &config_path, &label, !states.is_empty(), - // TODO: Get whether it's launched on startup. - false, + startup_config_paths.contains(&config_path), )?); } @@ -234,30 +246,34 @@ impl SysTray { info!("Exiting through system tray."); app_handle.exit(0); + Ok(()) } MenuEvent::ToggleWindowConfig { enable, path } => { - info!( - "Window config toggled {} at path {} enabled.", - enable, path - ); + info!("Window config '{}' to be enabled: {}", path, enable); - // let window_config = config - // .window_config_by_path(&path) - // .await? - // .context("Window config not found.")?; + match enable { + true => { + let window_config = config + .window_config_by_path(&path) + .await? + .context("Window config not found.")?; - // window_factory.open(window_config).await - Ok(()) + window_factory.open(window_config).await + } + false => window_factory.close_by_path(&path).await, + } } MenuEvent::ToggleStartupWindowConfig { enable, path } => { info!( - "Window config toggled {} at path {} set to launch on startup.", - enable, path + "Window config '{}' to be launched on startup: {}", + path, enable ); - // config.add_startup_config(path).await - Ok(()) + match enable { + true => config.add_startup_config(&path).await, + false => config.remove_startup_config(&path).await, + } } } } @@ -266,6 +282,7 @@ impl SysTray { async fn create_configs_menu( &self, window_states: &HashMap>, + startup_config_paths: &Vec, ) -> anyhow::Result> { let mut configs_menu = SubmenuBuilder::new(&self.app_handle, "Window configs"); @@ -279,8 +296,7 @@ impl SysTray { &window_config.config_path, &label, window_states.contains_key(&window_config.config_path), - // TODO: Get whether it's launched on startup. - false, + startup_config_paths.contains(&window_config.config_path), )?; configs_menu = configs_menu.item(&menu_item); @@ -300,7 +316,7 @@ impl SysTray { let enabled_item = CheckMenuItem::with_id( &self.app_handle, MenuEvent::ToggleWindowConfig { - enable: is_enabled, + enable: !is_enabled, path: config_path.to_string(), }, "Enabled", @@ -312,7 +328,7 @@ impl SysTray { let startup_item = CheckMenuItem::with_id( &self.app_handle, MenuEvent::ToggleStartupWindowConfig { - enable: is_launched_on_startup, + enable: !is_launched_on_startup, path: config_path.to_string(), }, "Launch on startup", From bed82f7e902ab5f43140a454d678059cfd003a14 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Sun, 8 Sep 2024 03:19:21 +0800 Subject: [PATCH 097/138] style: linting fixes --- packages/client-api/src/zebar.css | 1 - packages/desktop/src/providers/cpu/provider.rs | 4 +--- packages/desktop/src/providers/host/provider.rs | 4 +--- packages/desktop/src/providers/ip/provider.rs | 4 +--- packages/desktop/src/providers/network/provider.rs | 4 +--- packages/desktop/src/providers/weather/provider.rs | 4 +--- 6 files changed, 5 insertions(+), 16 deletions(-) diff --git a/packages/client-api/src/zebar.css b/packages/client-api/src/zebar.css index 5cb1d3c4..dbcd04a0 100644 --- a/packages/client-api/src/zebar.css +++ b/packages/client-api/src/zebar.css @@ -20,7 +20,6 @@ body { overscroll-behavior-x: none; } - /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ /* Document diff --git a/packages/desktop/src/providers/cpu/provider.rs b/packages/desktop/src/providers/cpu/provider.rs index 2ba5d206..7d5e7c59 100644 --- a/packages/desktop/src/providers/cpu/provider.rs +++ b/packages/desktop/src/providers/cpu/provider.rs @@ -4,9 +4,7 @@ use sysinfo::System; use tokio::sync::Mutex; use super::{CpuOutput, CpuProviderConfig}; -use crate::{ - impl_interval_provider, providers::ProviderOutput, -}; +use crate::{impl_interval_provider, providers::ProviderOutput}; pub struct CpuProvider { config: CpuProviderConfig, diff --git a/packages/desktop/src/providers/host/provider.rs b/packages/desktop/src/providers/host/provider.rs index dae9b7f3..d0be0164 100644 --- a/packages/desktop/src/providers/host/provider.rs +++ b/packages/desktop/src/providers/host/provider.rs @@ -1,9 +1,7 @@ use sysinfo::System; use super::{HostOutput, HostProviderConfig}; -use crate::{ - impl_interval_provider, providers::ProviderOutput, -}; +use crate::{impl_interval_provider, providers::ProviderOutput}; pub struct HostProvider { config: HostProviderConfig, diff --git a/packages/desktop/src/providers/ip/provider.rs b/packages/desktop/src/providers/ip/provider.rs index 61fe6d14..8ed18046 100644 --- a/packages/desktop/src/providers/ip/provider.rs +++ b/packages/desktop/src/providers/ip/provider.rs @@ -2,9 +2,7 @@ use anyhow::Context; use reqwest::Client; use super::{ipinfo_res::IpinfoRes, IpOutput, IpProviderConfig}; -use crate::{ - impl_interval_provider, providers::ProviderOutput, -}; +use crate::{impl_interval_provider, providers::ProviderOutput}; pub struct IpProvider { config: IpProviderConfig, diff --git a/packages/desktop/src/providers/network/provider.rs b/packages/desktop/src/providers/network/provider.rs index e6385c48..5d696ee7 100644 --- a/packages/desktop/src/providers/network/provider.rs +++ b/packages/desktop/src/providers/network/provider.rs @@ -9,9 +9,7 @@ use super::{ InterfaceType, NetworkGateway, NetworkInterface, NetworkOutput, NetworkProviderConfig, NetworkTraffic, }; -use crate::{ - impl_interval_provider, providers::ProviderOutput, -}; +use crate::{impl_interval_provider, providers::ProviderOutput}; pub struct NetworkProvider { config: NetworkProviderConfig, diff --git a/packages/desktop/src/providers/weather/provider.rs b/packages/desktop/src/providers/weather/provider.rs index 80362880..f3c6b305 100644 --- a/packages/desktop/src/providers/weather/provider.rs +++ b/packages/desktop/src/providers/weather/provider.rs @@ -4,9 +4,7 @@ use super::{ open_meteo_res::OpenMeteoRes, WeatherOutput, WeatherProviderConfig, WeatherStatus, }; -use crate::{ - impl_interval_provider, providers::ProviderOutput, -}; +use crate::{impl_interval_provider, providers::ProviderOutput}; pub struct WeatherProvider { config: WeatherProviderConfig, From 5c7c23fe81305d6600333c95d85df30c6a193e8e Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Sun, 8 Sep 2024 16:29:50 +0800 Subject: [PATCH 098/138] refactor: change config paths to be `PathBuf`'s instead of strings --- packages/desktop/src/cli.rs | 6 +-- packages/desktop/src/commands.rs | 4 +- packages/desktop/src/common/path_ext.rs | 24 +++++------ packages/desktop/src/config.rs | 45 +++++++++++---------- packages/desktop/src/main.rs | 5 ++- packages/desktop/src/sys_tray.rs | 54 ++++++++++++++++--------- packages/desktop/src/window_factory.rs | 30 +++++++++----- 7 files changed, 98 insertions(+), 70 deletions(-) diff --git a/packages/desktop/src/cli.rs b/packages/desktop/src/cli.rs index a932dbbe..aa0eec4f 100644 --- a/packages/desktop/src/cli.rs +++ b/packages/desktop/src/cli.rs @@ -47,9 +47,9 @@ pub enum CliCommand { pub struct OpenWindowArgs { /// Relative file path to window config within the Zebar config /// directory. - pub config_path: String, + pub config_path: PathBuf, - /// Absolute path to the Zebar config directory. + /// Absolute or relative path to the Zebar config directory. /// /// The default path is `%userprofile%/.glzr/zebar/` #[clap(long, value_hint = clap::ValueHint::FilePath)] @@ -58,7 +58,7 @@ pub struct OpenWindowArgs { #[derive(Args, Clone, Debug, PartialEq)] pub struct OpenAllWindowsArgs { - /// Absolute path to the Zebar config directory. + /// Absolute or relative path to the Zebar config directory. /// /// The default path is `%userprofile%/.glzr/zebar/` #[clap(long, value_hint = clap::ValueHint::FilePath)] diff --git a/packages/desktop/src/commands.rs b/packages/desktop/src/commands.rs index 219f761f..ed7b0f6b 100644 --- a/packages/desktop/src/commands.rs +++ b/packages/desktop/src/commands.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{path::PathBuf, sync::Arc}; use tauri::{State, Window}; @@ -24,7 +24,7 @@ pub async fn open_window( window_factory: State<'_, Arc>, ) -> anyhow::Result<(), String> { let window_config = config - .window_config_by_path(&config_path) + .window_config_by_path(&PathBuf::from(config_path)) .await .and_then(|opt| { opt.ok_or_else(|| anyhow::anyhow!("Window config not found.")) diff --git a/packages/desktop/src/common/path_ext.rs b/packages/desktop/src/common/path_ext.rs index f07b8945..ca3a394d 100644 --- a/packages/desktop/src/common/path_ext.rs +++ b/packages/desktop/src/common/path_ext.rs @@ -8,14 +8,14 @@ where Self: AsRef, { /// Like `std::fs::canonicalize()`, but on Windows it outputs paths - /// without UNC. + /// without UNC prefix. Similar to the `dunce::canonicalize` crate fn. /// /// Example: /// ``` /// let path = PathBuf::from("\\?\C:\\Users\\John\\Desktop\\test"); /// path.canonicalize_pretty().unwrap(); // "C:\\Users\\John\\Desktop\\test" /// ``` - fn canonicalize_pretty(&self) -> anyhow::Result; + fn to_absolute(&self) -> anyhow::Result; /// Converts the path to a unicode string. /// @@ -24,13 +24,12 @@ where } impl PathExt for PathBuf { - fn canonicalize_pretty(&self) -> anyhow::Result { + fn to_absolute(&self) -> anyhow::Result { let canonicalized = fs::canonicalize(self)?; - let canonicalized_str = canonicalized.to_unicode_string(); #[cfg(not(windows))] { - Ok(canonicalized_str) + Ok(canonicalized) } #[cfg(windows)] { @@ -42,16 +41,15 @@ impl PathExt for PathBuf { _ => false, }; - let stripped_str = if should_strip_unc { - canonicalized_str - .get(4..) - .map(Into::into) - .unwrap_or(canonicalized_str) - } else { - canonicalized_str + let formatted = match should_strip_unc { + true => canonicalized + .to_str() + .and_then(|path| path.get(4..)) + .map_or(canonicalized.clone(), PathBuf::from), + false => canonicalized, }; - Ok(stripped_str) + Ok(formatted) } } diff --git a/packages/desktop/src/config.rs b/packages/desktop/src/config.rs index 89fff0b9..4c2e0740 100644 --- a/packages/desktop/src/config.rs +++ b/packages/desktop/src/config.rs @@ -18,7 +18,7 @@ use crate::common::{ #[serde(rename_all = "camelCase")] pub struct SettingsConfig { /// Relative paths to window configs to launch on startup. - pub startup_configs: Vec, + pub startup_configs: Vec, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -128,11 +128,11 @@ pub struct Config { #[derive(Clone, Debug)] pub struct WindowConfigEntry { - /// Canonicalized absolute path to the window config file. - pub config_path: String, + /// Absolute path to the window config file. + pub config_path: PathBuf, - /// Canonicalized absolute path to the window's HTML file. - pub html_path: String, + /// Absolute path to the window's HTML file. + pub html_path: PathBuf, /// Parsed window config. pub config: WindowConfig, @@ -153,7 +153,7 @@ impl Config { .resolve(".glzr/zebar", BaseDirectory::Home) .context("Unable to get home directory.")?, } - .canonicalize_pretty()? + .to_absolute()? .into(); let settings = Self::read_settings_or_init(app_handle, &config_dir)?; @@ -269,15 +269,15 @@ impl Config { configs.extend(Self::read_window_configs(&path)?); } else if has_extension(&path, ".zebar.json") { if let Ok(config) = read_and_parse_json::(&path) { - let config_path = path.canonicalize_pretty()?; + let config_path = path.to_absolute()?; let html_path = path .parent() .context("Invalid parent directory.")? .join(&config.html_path) - .canonicalize_pretty()?; + .to_absolute()?; - info!("Found valid window config at: {}", config_path); + info!("Found valid window config at: {}", config_path.display()); configs.push(WindowConfigEntry { config, @@ -339,14 +339,14 @@ impl Config { /// Config path must be absolute. pub async fn add_startup_config( &self, - config_path: &str, + config_path: &PathBuf, ) -> anyhow::Result<()> { let startup_configs = self.startup_window_configs().await?; // Check if the config is already set to be launched on startup. if startup_configs .iter() - .find(|config| config.config_path == config_path) + .find(|config| config.config_path == *config_path) .is_some() { return Ok(()); @@ -356,7 +356,7 @@ impl Config { let mut new_settings = { self.settings.lock().await.clone() }; new_settings .startup_configs - .push(self.strip_config_dir(config_path)?.to_string()); + .push(self.strip_config_dir(config_path)?); self.write_settings(new_settings).await?; @@ -368,9 +368,9 @@ impl Config { /// Config path must be absolute. pub async fn remove_startup_config( &self, - config_path: &str, + config_path: &PathBuf, ) -> anyhow::Result<()> { - let rel_path = self.strip_config_dir(config_path)?.to_string(); + let rel_path = self.strip_config_dir(config_path)?; let mut new_settings = { self.settings.lock().await.clone() }; new_settings @@ -385,29 +385,30 @@ impl Config { /// Joins the given path with the config directory path. /// /// Returns an absolute path. - pub fn join_config_dir(&self, config_path: &str) -> String { - self.config_dir.join(config_path).to_unicode_string() + pub fn join_config_dir(&self, config_path: &PathBuf) -> PathBuf { + self.config_dir.join(config_path) } /// Strips the config directory path from the given path. /// /// Returns a relative path. - pub fn strip_config_dir<'a>( + pub fn strip_config_dir( &self, - config_path: &'a str, - ) -> anyhow::Result<&'a str> { + config_path: &PathBuf, + ) -> anyhow::Result { config_path - .strip_prefix(&self.config_dir.to_unicode_string()) + .strip_prefix(&self.config_dir) .context("Failed to strip config directory path.") + .map(Into::into) } /// Returns the window config at the given absolute path. pub async fn window_config_by_path( &self, - config_path: &str, + config_path: &PathBuf, ) -> anyhow::Result> { let formatted_config_path = - PathBuf::from(config_path).canonicalize_pretty()?; + PathBuf::from(config_path).to_absolute()?; let window_configs = self.window_configs.lock().await; let config_entry = window_configs diff --git a/packages/desktop/src/main.rs b/packages/desktop/src/main.rs index 68accd11..d440806e 100644 --- a/packages/desktop/src/main.rs +++ b/packages/desktop/src/main.rs @@ -209,7 +209,10 @@ async fn open_windows_by_cli_command( .window_config_by_path(&config.join_config_dir(&args.config_path)) .await? .with_context(|| { - format!("Window config not found at {}.", args.config_path) + format!( + "Window config not found at {}.", + args.config_path.display() + ) })?; vec![window_config] diff --git a/packages/desktop/src/sys_tray.rs b/packages/desktop/src/sys_tray.rs index c9710d43..9107940a 100644 --- a/packages/desktop/src/sys_tray.rs +++ b/packages/desktop/src/sys_tray.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, str::FromStr, sync::Arc}; +use std::{collections::HashMap, path::PathBuf, str::FromStr, sync::Arc}; use anyhow::{bail, Context}; use tauri::{ @@ -11,6 +11,7 @@ use tokio::task; use tracing::{error, info}; use crate::{ + common::PathExt, config::Config, window_factory::{WindowFactory, WindowState}, }; @@ -19,8 +20,8 @@ use crate::{ enum MenuEvent { ShowConfigFolder, Exit, - ToggleWindowConfig { enable: bool, path: String }, - ToggleStartupWindowConfig { enable: bool, path: String }, + ToggleWindowConfig { enable: bool, path: PathBuf }, + ToggleStartupWindowConfig { enable: bool, path: PathBuf }, } impl ToString for MenuEvent { @@ -29,10 +30,18 @@ impl ToString for MenuEvent { MenuEvent::ShowConfigFolder => "show_config_folder".to_string(), MenuEvent::Exit => "exit".to_string(), MenuEvent::ToggleWindowConfig { enable, path } => { - format!("toggle_window_config_{}_{}", enable, path) + format!( + "toggle_window_config_{}_{}", + enable, + path.to_unicode_string() + ) } MenuEvent::ToggleStartupWindowConfig { enable, path } => { - format!("toggle_startup_window_config_{}_{}", enable, path) + format!( + "toggle_startup_window_config_{}_{}", + enable, + path.to_unicode_string() + ) } } } @@ -50,13 +59,13 @@ impl FromStr for MenuEvent { ["toggle", "window", "config", enable @ ("true" | "false"), path @ ..] => { Ok(Self::ToggleWindowConfig { enable: *enable == "true", - path: path.join("_"), + path: PathBuf::from(path.join("_")), }) } ["toggle", "startup", "window", "config", enable @ ("true" | "false"), path @ ..] => { Ok(Self::ToggleStartupWindowConfig { enable: *enable == "true", - path: path.join("_"), + path: PathBuf::from(path.join("_")), }) } _ => bail!("Invalid menu event: {}", event), @@ -250,7 +259,11 @@ impl SysTray { Ok(()) } MenuEvent::ToggleWindowConfig { enable, path } => { - info!("Window config '{}' to be enabled: {}", path, enable); + info!( + "Window config '{}' to be enabled: {}", + path.display(), + enable + ); match enable { true => { @@ -267,7 +280,8 @@ impl SysTray { MenuEvent::ToggleStartupWindowConfig { enable, path } => { info!( "Window config '{}' to be launched on startup: {}", - path, enable + path.display(), + enable ); match enable { @@ -281,8 +295,8 @@ impl SysTray { /// Creates and returns a submenu for the window configs. async fn create_configs_menu( &self, - window_states: &HashMap>, - startup_config_paths: &Vec, + window_states: &HashMap>, + startup_config_paths: &Vec, ) -> anyhow::Result> { let mut configs_menu = SubmenuBuilder::new(&self.app_handle, "Window configs"); @@ -308,7 +322,7 @@ impl SysTray { /// Creates and returns a submenu for the given window config. fn create_config_menu( &self, - config_path: &str, + config_path: &PathBuf, label: &str, is_enabled: bool, is_launched_on_startup: bool, @@ -317,7 +331,7 @@ impl SysTray { &self.app_handle, MenuEvent::ToggleWindowConfig { enable: !is_enabled, - path: config_path.to_string(), + path: config_path.clone(), }, "Enabled", true, @@ -329,7 +343,7 @@ impl SysTray { &self.app_handle, MenuEvent::ToggleStartupWindowConfig { enable: !is_launched_on_startup, - path: config_path.to_string(), + path: config_path.clone(), }, "Launch on startup", true, @@ -347,13 +361,13 @@ impl SysTray { /// Formats the config path for display in the system tray. fn format_config_path( config: &Arc, - config_path: &str, + config_path: &PathBuf, ) -> String { - config + let path = config .strip_config_dir(config_path) - .ok() - .and_then(|path| path.strip_suffix(".zebar.json")) - .unwrap_or(config_path) - .into() + .unwrap_or(config_path.clone()) + .to_unicode_string(); + + path.strip_suffix(".zebar.json").unwrap_or(&path).into() } } diff --git a/packages/desktop/src/window_factory.rs b/packages/desktop/src/window_factory.rs index cc3bd7e3..359efed5 100644 --- a/packages/desktop/src/window_factory.rs +++ b/packages/desktop/src/window_factory.rs @@ -1,5 +1,6 @@ use std::{ collections::HashMap, + path::PathBuf, sync::{ atomic::{AtomicU32, Ordering}, Arc, @@ -18,7 +19,7 @@ use tokio::{ use tracing::{error, info}; use crate::{ - common::WindowExt, + common::{PathExt, WindowExt}, config::{WindowAnchor, WindowConfig, WindowConfigEntry}, monitor_state::MonitorState, }; @@ -60,10 +61,10 @@ pub struct WindowState { pub config: WindowConfig, /// Absolute path to the window's config file. - pub config_path: String, + pub config_path: PathBuf, /// Absolute path to the window's HTML file. - pub html_path: String, + pub html_path: PathBuf, } pub struct WindowPlacement { @@ -111,15 +112,26 @@ impl WindowFactory { self.window_count.fetch_add(1, Ordering::Relaxed) + 1; let window_id = new_count.to_string(); - info!("Creating window #{} from {}", new_count, config_path); + info!( + "Creating window #{} from {}", + new_count, + config_path.display() + ); + + // TODO: Url-encode the HTML path to get this working on MacOS/Linux. + let webview_url = WebviewUrl::App( + format!( + "http://asset.localhost/{}", + html_path.to_unicode_string() + ) + .into(), + ); // Note that window label needs to be globally unique. let window = WebviewWindowBuilder::new( &self.app_handle, window_id.clone(), - WebviewUrl::App( - format!("http://asset.localhost/{}", html_path).into(), - ), + webview_url, ) .title("Zebar") .inner_size(placement.width, placement.height) @@ -268,7 +280,7 @@ impl WindowFactory { /// Closes all windows with the given config path. pub async fn close_by_path( &self, - config_path: &str, + config_path: &PathBuf, ) -> anyhow::Result<()> { let window_states = self.states_by_config_path().await; @@ -291,7 +303,7 @@ impl WindowFactory { /// Returns window states grouped by their config paths. pub async fn states_by_config_path( &self, - ) -> HashMap> { + ) -> HashMap> { self.window_states.lock().await.values().fold( HashMap::new(), |mut acc, state| { From 0c92b6dd3b7e320bfff68fe63608b5de7b17c06b Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Mon, 9 Sep 2024 16:46:37 +0800 Subject: [PATCH 099/138] feat: add `package.json` metadata for client api --- packages/client-api/package.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/client-api/package.json b/packages/client-api/package.json index 5629e18c..32e461b2 100644 --- a/packages/client-api/package.json +++ b/packages/client-api/package.json @@ -1,6 +1,10 @@ { "name": "zebar", "version": "2.0.0", + "description": "Client API for Zebar - a tool for creating customizable taskbars, desktop widgets, and popups.", + "repository": "github:glzr-io/zebar", + "license": "GPL-3.0-only", + "author": "Lars Berger", "sideEffects": false, "exports": { ".": { @@ -12,6 +16,10 @@ "main": "./dist/index.js", "module": "./dist/index.mjs", "types": "./dist/index.d.ts", + "files": [ + "dist", + "README.md" + ], "scripts": { "build": "tsup src/index.ts --format cjs,esm --dts --inject-style", "dev": "npm run build -- --watch src", From 1878d2ed6e403d4b2c257337dbe524fbe32e483f Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Mon, 9 Sep 2024 16:46:49 +0800 Subject: [PATCH 100/138] feat: add incomplete react-buildless example --- examples/react-buildless/index.html | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 examples/react-buildless/index.html diff --git a/examples/react-buildless/index.html b/examples/react-buildless/index.html new file mode 100644 index 00000000..21398474 --- /dev/null +++ b/examples/react-buildless/index.html @@ -0,0 +1,17 @@ + + + + + + +
+ + + + From 9f16721954235edb63b2fca8c15bef53906eef23 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Mon, 9 Sep 2024 19:26:52 +0800 Subject: [PATCH 101/138] feat: add working react-buildless example --- examples/react-buildless/index.html | 58 ++++++++++++++++--- examples/react-buildless/my-window.zebar.json | 23 ++++++++ 2 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 examples/react-buildless/my-window.zebar.json diff --git a/examples/react-buildless/index.html b/examples/react-buildless/index.html index 21398474..ed168e57 100644 --- a/examples/react-buildless/index.html +++ b/examples/react-buildless/index.html @@ -1,17 +1,59 @@ - - - -
- + + + + diff --git a/examples/react-buildless/my-window.zebar.json b/examples/react-buildless/my-window.zebar.json new file mode 100644 index 00000000..0abb844f --- /dev/null +++ b/examples/react-buildless/my-window.zebar.json @@ -0,0 +1,23 @@ +{ + "$schema": "TODO", + "htmlPath": "./index.html", + "launchOptions": { + "zOrder": "normal", + "shownInTaskbar": false, + "focused": false, + "resizable": false, + "transparent": false, + "placements": [ + { + "anchor": "top_left", + "offsetX": "0px", + "offsetY": "0px", + "width": "100%", + "height": "50px", + "monitorSelection": { + "type": "all" + } + } + ] + } +} From 3c519837ed59289925111408e6ef2d318675afd9 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Mon, 9 Sep 2024 21:26:27 +0800 Subject: [PATCH 102/138] chore: remove commonjs build --- packages/client-api/package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/client-api/package.json b/packages/client-api/package.json index 32e461b2..aefae8c9 100644 --- a/packages/client-api/package.json +++ b/packages/client-api/package.json @@ -6,14 +6,13 @@ "license": "GPL-3.0-only", "author": "Lars Berger", "sideEffects": false, + "type": "module", "exports": { ".": { - "require": "./dist/index.js", "import": "./dist/index.mjs", "types": "./dist/index.d.ts" } }, - "main": "./dist/index.js", "module": "./dist/index.mjs", "types": "./dist/index.d.ts", "files": [ @@ -21,7 +20,7 @@ "README.md" ], "scripts": { - "build": "tsup src/index.ts --format cjs,esm --dts --inject-style", + "build": "tsup src/index.ts --format esm --dts --inject-style", "dev": "npm run build -- --watch src", "prepublishOnly": "npm run build" }, From bd4b0694852d1202ce08ae65a8fc088fd414c371 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Mon, 9 Sep 2024 21:36:41 +0800 Subject: [PATCH 103/138] feat: remove style injection; instead create style element manually --- packages/client-api/package.json | 2 +- packages/client-api/src/init.ts | 6 ++++- .../src/{zebar.css => zebar-styles.ts} | 27 ++++++++++++------- 3 files changed, 23 insertions(+), 12 deletions(-) rename packages/client-api/src/{zebar.css => zebar-styles.ts} (88%) diff --git a/packages/client-api/package.json b/packages/client-api/package.json index aefae8c9..f3df8e92 100644 --- a/packages/client-api/package.json +++ b/packages/client-api/package.json @@ -20,7 +20,7 @@ "README.md" ], "scripts": { - "build": "tsup src/index.ts --format esm --dts --inject-style", + "build": "tsup src/index.ts --format esm --dts", "dev": "npm run build -- --watch src", "prepublishOnly": "npm run build" }, diff --git a/packages/client-api/src/init.ts b/packages/client-api/src/init.ts index 2d1b1612..36c9b257 100644 --- a/packages/client-api/src/init.ts +++ b/packages/client-api/src/init.ts @@ -10,6 +10,7 @@ import { import { createLogger } from '~/utils'; import type { ZebarContext } from './zebar-context.model'; import { createProvider } from './providers'; +import { zebarStyles } from './zebar-styles'; const logger = createLogger('init-window'); @@ -42,7 +43,10 @@ export async function init( // Load default CSS unless explicitly disabled. if (options?.includeDefaultCss !== false) { - import('./zebar.css'); + const styleElement = document.createElement('style'); + styleElement.setAttribute('data-zebar', ''); + styleElement.innerHTML = zebarStyles; + document.head.appendChild(styleElement); } // @ts-ignore - TODO diff --git a/packages/client-api/src/zebar.css b/packages/client-api/src/zebar-styles.ts similarity index 88% rename from packages/client-api/src/zebar.css rename to packages/client-api/src/zebar-styles.ts index dbcd04a0..bc1dfd54 100644 --- a/packages/client-api/src/zebar.css +++ b/packages/client-api/src/zebar-styles.ts @@ -1,9 +1,15 @@ +import type { ZebarInitOptions } from './init'; + +/** + * Default CSS if {@link ZebarInitOptions.includeDefaultCss} is enabled. + */ +export const zebarStyles = ` html { box-sizing: border-box; } /** - * Set default `box-sizing` for all elements to border-box. + * Set default \`box-sizing\` for all elements to border-box. */ *, *:before, @@ -47,7 +53,7 @@ body { } /** - * Render the `main` element consistently in IE. + * Render the \`main\` element consistently in IE. */ main { @@ -55,8 +61,8 @@ main { } /** - * Correct the font size and margin on `h1` elements within `section` and - * `article` contexts in Chrome, Firefox, and Safari. + * Correct the font size and margin on \`h1\` elements within \`section\` and + * \`article\` contexts in Chrome, Firefox, and Safari. */ h1 { @@ -80,7 +86,7 @@ hr { /** * 1. Correct the inheritance and scaling of font size in all browsers. - * 2. Correct the odd `em` font sizing in all browsers. + * 2. Correct the odd \`em\` font sizing in all browsers. */ pre { @@ -121,7 +127,7 @@ strong { /** * 1. Correct the inheritance and scaling of font size in all browsers. - * 2. Correct the odd `em` font sizing in all browsers. + * 2. Correct the odd \`em\` font sizing in all browsers. */ code, @@ -140,7 +146,7 @@ small { } /** - * Prevent `sub` and `sup` elements from affecting the line height in + * Prevent \`sub\` and \`sup\` elements from affecting the line height in * all browsers. */ @@ -256,9 +262,9 @@ fieldset { /** * 1. Correct the text wrapping in Edge and IE. - * 2. Correct the color inheritance from `fieldset` elements in IE. + * 2. Correct the color inheritance from \`fieldset\` elements in IE. * 3. Remove the padding so developers are not caught out when they zero out - * `fieldset` elements in all browsers. + * \`fieldset\` elements in all browsers. */ legend { @@ -326,7 +332,7 @@ textarea { /** * 1. Correct the inability to style clickable types in iOS and Safari. - * 2. Change font properties to `inherit` in Safari. + * 2. Change font properties to \`inherit\` in Safari. */ ::-webkit-file-upload-button { @@ -371,3 +377,4 @@ template { [hidden] { display: none; } +`; From 043122ac05de83c8528c1543392062102c3244c8 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Mon, 9 Sep 2024 21:46:44 +0800 Subject: [PATCH 104/138] chore: fix entrypoint after removing commonjs --- packages/client-api/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/client-api/package.json b/packages/client-api/package.json index f3df8e92..b5aad18c 100644 --- a/packages/client-api/package.json +++ b/packages/client-api/package.json @@ -9,11 +9,11 @@ "type": "module", "exports": { ".": { - "import": "./dist/index.mjs", + "import": "./dist/index.js", "types": "./dist/index.d.ts" } }, - "module": "./dist/index.mjs", + "module": "./dist/index.js", "types": "./dist/index.d.ts", "files": [ "dist", From faef3b16bb30fcfc1ca288f890f1427d8ead6eff Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Tue, 10 Sep 2024 02:17:26 +0800 Subject: [PATCH 105/138] feat: use `examples/` dir as default window configs; add default startup depending on whether glazewm is installed --- .gitignore | 1 - .../starter/vanilla.html | 0 .../starter/vanilla.zebar.json | 2 +- examples/starter/with-glazewm.html | 21 ++++++ .../starter/with-glazewm.zebar.json | 2 +- .../starter/with-komorebi.html | 0 examples/starter/with-komorebi.zebar.json | 23 ++++++ .../desktop/resources/config/settings.json | 4 - packages/desktop/src/config.rs | 73 +++++++++++++++++-- packages/desktop/src/window_factory.rs | 10 ++- packages/desktop/tauri.conf.json | 2 +- 11 files changed, 123 insertions(+), 15 deletions(-) rename packages/desktop/resources/config/starter/glazewm.html => examples/starter/vanilla.html (100%) rename packages/desktop/resources/config/starter/glazewm.zebar.json => examples/starter/vanilla.zebar.json (92%) create mode 100644 examples/starter/with-glazewm.html rename packages/desktop/resources/config/starter/komorebi.zebar.json => examples/starter/with-glazewm.zebar.json (91%) rename packages/desktop/resources/config/starter/komorebi.html => examples/starter/with-komorebi.html (100%) create mode 100644 examples/starter/with-komorebi.zebar.json delete mode 100644 packages/desktop/resources/config/settings.json diff --git a/.gitignore b/.gitignore index 6941e923..94d9deec 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,6 @@ gen *.pem *.env examples/settings.json -examples/starter # Debug npm-debug.log* diff --git a/packages/desktop/resources/config/starter/glazewm.html b/examples/starter/vanilla.html similarity index 100% rename from packages/desktop/resources/config/starter/glazewm.html rename to examples/starter/vanilla.html diff --git a/packages/desktop/resources/config/starter/glazewm.zebar.json b/examples/starter/vanilla.zebar.json similarity index 92% rename from packages/desktop/resources/config/starter/glazewm.zebar.json rename to examples/starter/vanilla.zebar.json index f782022c..7a5584c5 100644 --- a/packages/desktop/resources/config/starter/glazewm.zebar.json +++ b/examples/starter/vanilla.zebar.json @@ -1,6 +1,6 @@ { "$schema": "TODO", - "htmlPath": "./glazewm.html", + "htmlPath": "./vanilla.html", "launchOptions": { "zOrder": "normal", "shownInTaskbar": false, diff --git a/examples/starter/with-glazewm.html b/examples/starter/with-glazewm.html new file mode 100644 index 00000000..cb79ec27 --- /dev/null +++ b/examples/starter/with-glazewm.html @@ -0,0 +1,21 @@ + + +

fjdisa

+ + + diff --git a/packages/desktop/resources/config/starter/komorebi.zebar.json b/examples/starter/with-glazewm.zebar.json similarity index 91% rename from packages/desktop/resources/config/starter/komorebi.zebar.json rename to examples/starter/with-glazewm.zebar.json index 15a517fb..0ce8cc53 100644 --- a/packages/desktop/resources/config/starter/komorebi.zebar.json +++ b/examples/starter/with-glazewm.zebar.json @@ -1,6 +1,6 @@ { "$schema": "TODO", - "htmlPath": "./komorebi.html", + "htmlPath": "./with-glazewm.html", "launchOptions": { "zOrder": "normal", "shownInTaskbar": false, diff --git a/packages/desktop/resources/config/starter/komorebi.html b/examples/starter/with-komorebi.html similarity index 100% rename from packages/desktop/resources/config/starter/komorebi.html rename to examples/starter/with-komorebi.html diff --git a/examples/starter/with-komorebi.zebar.json b/examples/starter/with-komorebi.zebar.json new file mode 100644 index 00000000..1cdeb957 --- /dev/null +++ b/examples/starter/with-komorebi.zebar.json @@ -0,0 +1,23 @@ +{ + "$schema": "TODO", + "htmlPath": "./with-komorebi.html", + "launchOptions": { + "zOrder": "normal", + "shownInTaskbar": false, + "focused": false, + "resizable": false, + "transparent": false, + "placements": [ + { + "anchor": "top_left", + "offsetX": "20px", + "offsetY": "20px", + "width": "100%", + "height": "30px", + "monitorSelection": { + "type": "all" + } + } + ] + } +} diff --git a/packages/desktop/resources/config/settings.json b/packages/desktop/resources/config/settings.json deleted file mode 100644 index 10de35f3..00000000 --- a/packages/desktop/resources/config/settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "TODO", - "startupConfigs": ["starter/glazewm.zebar.json"] -} diff --git a/packages/desktop/src/config.rs b/packages/desktop/src/config.rs index 4c2e0740..4d458d15 100644 --- a/packages/desktop/src/config.rs +++ b/packages/desktop/src/config.rs @@ -17,6 +17,10 @@ use crate::common::{ #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct SettingsConfig { + /// JSON schema URL to validate the settings file. + #[serde(rename = "$schema")] + schema: Option, + /// Relative paths to window configs to launch on startup. pub startup_configs: Vec, } @@ -24,6 +28,10 @@ pub struct SettingsConfig { #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct WindowConfig { + /// JSON schema URL to validate the window config file. + #[serde(rename = "$schema")] + schema: Option, + /// Entry point HTML file. pub html_path: String, @@ -203,7 +211,7 @@ impl Config { match settings { Some(settings) => Ok(settings), None => { - Self::create_from_starter(app_handle, dir)?; + Self::create_from_examples(app_handle, dir)?; Ok( Self::read_settings(&dir)? @@ -293,18 +301,44 @@ impl Config { Ok(configs) } - /// Initializes config at the given path from the starter resource. - fn create_from_starter( + /// Initializes settings and window configs at the given path. + /// + /// `settings.json` is initialized with either `starter/vanilla.json` or + /// `starter/with-glazewm.json` as startup config. Window configs are + /// initialized from `examples/` directory. + fn create_from_examples( app_handle: &AppHandle, config_dir: &PathBuf, ) -> anyhow::Result<()> { let starter_path = app_handle .path() - .resolve("resources/config", BaseDirectory::Resource) + .resolve("../../examples", BaseDirectory::Resource) .context("Unable to resolve starter config resource.")?; + info!( + "Copying starter configs from {} to {}.", + starter_path.display(), + config_dir.display() + ); + copy_dir_all(&starter_path, config_dir, false)?; + let default_startup_config = match is_app_installed("glazewm") { + true => "starter/with-glazewm.zebar.json", + false => "starter/vanilla.zebar.json", + }; + + let default_settings = SettingsConfig { + schema: Some("TODO".into()), + startup_configs: vec![default_startup_config.into()], + }; + + let settings_path = config_dir.join("settings.json"); + fs::write( + &settings_path, + serde_json::to_string_pretty(&default_settings)?, + )?; + Ok(()) } @@ -322,11 +356,15 @@ impl Config { let mut result = Vec::new(); for config_path in startup_configs { + let abs_config_path = self.join_config_dir(&config_path); let config = self - .window_config_by_path(&self.join_config_dir(&config_path)) + .window_config_by_path(&abs_config_path) .await .unwrap_or(None) - .context("Failed to get window config.")?; + .context(format!( + "Failed to get window config at {}.", + abs_config_path.display() + ))?; result.push(config); } @@ -445,3 +483,26 @@ impl Config { Ok(()) } } + +/// Checks if an application is installed and available in the system PATH. +/// +/// Returns `true` if the application is found in PATH, `false` otherwise. +fn is_app_installed(app_name: &str) -> bool { + #[cfg(target_os = "windows")] + { + std::process::Command::new("where") + .arg(app_name) + .output() + .map(|output| output.status.success()) + .unwrap_or(false) + } + + #[cfg(any(target_os = "macos", target_os = "linux"))] + { + std::process::Command::new("which") + .arg(app_name) + .output() + .map(|output| output.status.success()) + .unwrap_or(false) + } +} diff --git a/packages/desktop/src/window_factory.rs b/packages/desktop/src/window_factory.rs index 359efed5..9769ccf6 100644 --- a/packages/desktop/src/window_factory.rs +++ b/packages/desktop/src/window_factory.rs @@ -7,7 +7,7 @@ use std::{ }, }; -use anyhow::Context; +use anyhow::{bail, Context}; use serde::Serialize; use tauri::{ AppHandle, Manager, WebviewUrl, WebviewWindowBuilder, WindowEvent, @@ -112,6 +112,14 @@ impl WindowFactory { self.window_count.fetch_add(1, Ordering::Relaxed) + 1; let window_id = new_count.to_string(); + if !html_path.exists() { + bail!( + "HTML file not found at {} for config {}.", + html_path.display(), + config_path.display() + ); + } + info!( "Creating window #{} from {}", new_count, diff --git a/packages/desktop/tauri.conf.json b/packages/desktop/tauri.conf.json index b8318583..46b4603f 100644 --- a/packages/desktop/tauri.conf.json +++ b/packages/desktop/tauri.conf.json @@ -24,7 +24,7 @@ "shortDescription": "Zebar", "category": "Utility", "publisher": "Glzr Software Pte. Ltd.", - "resources": ["resources/*"], + "resources": ["resources/*", "../../examples/*"], "targets": ["deb", "appimage", "msi", "dmg"], "windows": { "signCommand": "powershell -ExecutionPolicy Bypass -File ./resources/scripts/sign.ps1 -FilePath %1", From 9ee8cc8c572dd28fcb73fdc1f135284a2667db99 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Tue, 10 Sep 2024 02:21:28 +0800 Subject: [PATCH 106/138] feat: move boilerplates to `examples/boilerplates` --- .../react-buildless/index.html | 0 .../react-buildless/my-window.zebar.json | 0 .../solid-ts}/.gitignore | 0 .../solid-ts}/README.md | 0 .../solid-ts}/index.html | 0 .../solid-ts}/my-window.zebar.json | 0 .../solid-ts}/package.json | 0 .../solid-ts}/src/index.css | 0 .../solid-ts}/src/index.tsx | 0 .../solid-ts}/tsconfig.json | 0 .../solid-ts}/vite.config.ts | 0 examples/solidjs-buildless/index.html | 20 ---------------- .../solidjs-buildless/my-window.zebar.json | 23 ------------------- pnpm-lock.yaml | 4 ++-- pnpm-workspace.yaml | 3 ++- 15 files changed, 4 insertions(+), 46 deletions(-) rename examples/{ => boilerplates}/react-buildless/index.html (100%) rename examples/{ => boilerplates}/react-buildless/my-window.zebar.json (100%) rename examples/{solidjs-ts => boilerplates/solid-ts}/.gitignore (100%) rename examples/{solidjs-ts => boilerplates/solid-ts}/README.md (100%) rename examples/{solidjs-ts => boilerplates/solid-ts}/index.html (100%) rename examples/{solidjs-ts => boilerplates/solid-ts}/my-window.zebar.json (100%) rename examples/{solidjs-ts => boilerplates/solid-ts}/package.json (100%) rename examples/{solidjs-ts => boilerplates/solid-ts}/src/index.css (100%) rename examples/{solidjs-ts => boilerplates/solid-ts}/src/index.tsx (100%) rename examples/{solidjs-ts => boilerplates/solid-ts}/tsconfig.json (100%) rename examples/{solidjs-ts => boilerplates/solid-ts}/vite.config.ts (100%) delete mode 100644 examples/solidjs-buildless/index.html delete mode 100644 examples/solidjs-buildless/my-window.zebar.json diff --git a/examples/react-buildless/index.html b/examples/boilerplates/react-buildless/index.html similarity index 100% rename from examples/react-buildless/index.html rename to examples/boilerplates/react-buildless/index.html diff --git a/examples/react-buildless/my-window.zebar.json b/examples/boilerplates/react-buildless/my-window.zebar.json similarity index 100% rename from examples/react-buildless/my-window.zebar.json rename to examples/boilerplates/react-buildless/my-window.zebar.json diff --git a/examples/solidjs-ts/.gitignore b/examples/boilerplates/solid-ts/.gitignore similarity index 100% rename from examples/solidjs-ts/.gitignore rename to examples/boilerplates/solid-ts/.gitignore diff --git a/examples/solidjs-ts/README.md b/examples/boilerplates/solid-ts/README.md similarity index 100% rename from examples/solidjs-ts/README.md rename to examples/boilerplates/solid-ts/README.md diff --git a/examples/solidjs-ts/index.html b/examples/boilerplates/solid-ts/index.html similarity index 100% rename from examples/solidjs-ts/index.html rename to examples/boilerplates/solid-ts/index.html diff --git a/examples/solidjs-ts/my-window.zebar.json b/examples/boilerplates/solid-ts/my-window.zebar.json similarity index 100% rename from examples/solidjs-ts/my-window.zebar.json rename to examples/boilerplates/solid-ts/my-window.zebar.json diff --git a/examples/solidjs-ts/package.json b/examples/boilerplates/solid-ts/package.json similarity index 100% rename from examples/solidjs-ts/package.json rename to examples/boilerplates/solid-ts/package.json diff --git a/examples/solidjs-ts/src/index.css b/examples/boilerplates/solid-ts/src/index.css similarity index 100% rename from examples/solidjs-ts/src/index.css rename to examples/boilerplates/solid-ts/src/index.css diff --git a/examples/solidjs-ts/src/index.tsx b/examples/boilerplates/solid-ts/src/index.tsx similarity index 100% rename from examples/solidjs-ts/src/index.tsx rename to examples/boilerplates/solid-ts/src/index.tsx diff --git a/examples/solidjs-ts/tsconfig.json b/examples/boilerplates/solid-ts/tsconfig.json similarity index 100% rename from examples/solidjs-ts/tsconfig.json rename to examples/boilerplates/solid-ts/tsconfig.json diff --git a/examples/solidjs-ts/vite.config.ts b/examples/boilerplates/solid-ts/vite.config.ts similarity index 100% rename from examples/solidjs-ts/vite.config.ts rename to examples/boilerplates/solid-ts/vite.config.ts diff --git a/examples/solidjs-buildless/index.html b/examples/solidjs-buildless/index.html deleted file mode 100644 index 78f0bacc..00000000 --- a/examples/solidjs-buildless/index.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - diff --git a/examples/solidjs-buildless/my-window.zebar.json b/examples/solidjs-buildless/my-window.zebar.json deleted file mode 100644 index 54a726b0..00000000 --- a/examples/solidjs-buildless/my-window.zebar.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "TODO", - "htmlPath": "./index.html", - "launchOptions": { - "zOrder": "normal", - "shownInTaskbar": false, - "focused": false, - "resizable": false, - "transparent": false, - "placements": [ - { - "anchor": "top_left", - "offsetX": "20px", - "offsetY": "20px", - "width": "100%", - "height": "30px", - "monitorSelection": { - "type": "all" - } - } - ] - } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4ad4071c..d3aa90d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,14 +15,14 @@ importers: specifier: 3.2.5 version: 3.2.5 - examples/solidjs-ts: + examples/boilerplates/solid-ts: dependencies: solid-js: specifier: 1.8.11 version: 1.8.11 zebar: specifier: ^2.0.0 - version: link:../../packages/client-api + version: link:../../../packages/client-api devDependencies: typescript: specifier: 5.3.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 15989339..a68fdeb6 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,4 @@ packages: - 'packages/*' - - 'examples/*' + - 'examples/starter/*' + - 'examples/boilerplates/*' From 26dcc8899b78c0e1d3355eb4cbb4c6129df7267a Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Wed, 11 Sep 2024 02:29:16 +0800 Subject: [PATCH 107/138] feat: add `hasBattery` to battery output; change battery output properties to nullable --- .../battery/create-battery-provider.ts | 9 ++-- .../desktop/src/providers/battery/provider.rs | 53 ++++++++++++------- .../src/providers/battery/variables.rs | 9 ++-- 3 files changed, 44 insertions(+), 27 deletions(-) diff --git a/packages/client-api/src/providers/battery/create-battery-provider.ts b/packages/client-api/src/providers/battery/create-battery-provider.ts index d1b43807..0013129c 100644 --- a/packages/client-api/src/providers/battery/create-battery-provider.ts +++ b/packages/client-api/src/providers/battery/create-battery-provider.ts @@ -26,10 +26,11 @@ export type BatteryProvider = Provider< >; export interface BatteryOutput { - chargePercent: number; - cycleCount: number; - healthPercent: number; - powerConsumption: number; + hasBattery: boolean; + chargePercent: number | null; + cycleCount: number | null; + healthPercent: number | null; + powerConsumption: number | null; state: 'discharging' | 'charging' | 'full' | 'empty' | 'unknown'; isCharging: boolean; timeTillEmpty: number | null; diff --git a/packages/desktop/src/providers/battery/provider.rs b/packages/desktop/src/providers/battery/provider.rs index d716a77c..4f5f7c26 100644 --- a/packages/desktop/src/providers/battery/provider.rs +++ b/packages/desktop/src/providers/battery/provider.rs @@ -1,4 +1,3 @@ -use anyhow::Context; use starship_battery::{ units::{ electric_potential::volt, power::watt, ratio::percent, @@ -27,24 +26,40 @@ impl BatteryProvider { let battery = Manager::new()? .batteries() .and_then(|mut batteries| batteries.nth(0).transpose()) - .unwrap_or(None) - .context("No battery found.")?; - - Ok(ProviderOutput::Battery(BatteryOutput { - charge_percent: battery.state_of_charge().get::(), - health_percent: battery.state_of_health().get::(), - state: battery.state().to_string(), - is_charging: battery.state() == State::Charging, - time_till_full: battery - .time_to_full() - .map(|time| time.get::()), - time_till_empty: battery - .time_to_empty() - .map(|time| time.get::()), - power_consumption: battery.energy_rate().get::(), - voltage: battery.voltage().get::(), - cycle_count: battery.cycle_count(), - })) + .unwrap_or(None); + + let output = match battery { + None => BatteryOutput { + has_battery: false, + charge_percent: None, + health_percent: None, + state: "unknown".to_string(), + is_charging: false, + time_till_full: None, + time_till_empty: None, + power_consumption: None, + voltage: None, + cycle_count: None, + }, + Some(battery) => BatteryOutput { + has_battery: true, + charge_percent: Some(battery.state_of_charge().get::()), + health_percent: Some(battery.state_of_health().get::()), + state: battery.state().to_string(), + is_charging: battery.state() == State::Charging, + time_till_full: battery + .time_to_full() + .map(|time| time.get::()), + time_till_empty: battery + .time_to_empty() + .map(|time| time.get::()), + power_consumption: Some(battery.energy_rate().get::()), + voltage: Some(battery.voltage().get::()), + cycle_count: battery.cycle_count(), + }, + }; + + Ok(ProviderOutput::Battery(output)) } } diff --git a/packages/desktop/src/providers/battery/variables.rs b/packages/desktop/src/providers/battery/variables.rs index dbb4cf57..186cd995 100644 --- a/packages/desktop/src/providers/battery/variables.rs +++ b/packages/desktop/src/providers/battery/variables.rs @@ -3,13 +3,14 @@ use serde::Serialize; #[derive(Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct BatteryOutput { - pub charge_percent: f32, - pub health_percent: f32, + pub has_battery: bool, + pub charge_percent: Option, + pub health_percent: Option, pub state: String, pub is_charging: bool, pub time_till_full: Option, pub time_till_empty: Option, - pub power_consumption: f32, - pub voltage: f32, + pub power_consumption: Option, + pub voltage: Option, pub cycle_count: Option, } From 4a189093c77f36e458fb2d5b286de4644ac6abe9 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Wed, 11 Sep 2024 02:29:51 +0800 Subject: [PATCH 108/138] feat: implement date provider formatting --- .../providers/date/create-date-provider.ts | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/packages/client-api/src/providers/date/create-date-provider.ts b/packages/client-api/src/providers/date/create-date-provider.ts index 668153b4..063614bd 100644 --- a/packages/client-api/src/providers/date/create-date-provider.ts +++ b/packages/client-api/src/providers/date/create-date-provider.ts @@ -16,23 +16,24 @@ export interface DateProviderConfig { /** * Either a UTC offset (eg. `UTC+8`) or an IANA timezone (eg. - * `America/New_York`). Affects the output of `toFormat()`. + * `America/New_York`). Affects the output of {@link DateOutput.formatted}. * * A full list of available IANA timezones can be found [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List). */ timezone?: string; /** - * An ISO-639-1 locale, which is either a 2-letter language code (eg. `en`) or - * 4-letter language + country code (eg. `en-gb`). Affects the output of - * `toFormat()`. + * An ISO-639-1 locale, which is either a 2-letter language code + * (eg. `en`) or a 4-letter language + country code (eg. `en-gb`). + * Affects the output of {@link DateOutput.formatted}. * * A full list of ISO-639-1 locales can be found [here](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes#Table). */ locale?: string; /** - * Formatting of the current date into a custom string format. + * Formatting of the current date into a custom string format. Affects + * the output of {@link DateOutput.formatted}. * * Refer to [table of tokens](https://moment.github.io/luxon/#/formatting?id=table-of-tokens) * for available date/time tokens. @@ -47,13 +48,19 @@ export interface DateProviderConfig { const dateProviderConfigSchema = z.object({ type: z.literal('date'), refreshInterval: z.coerce.number().default(1000), - timezone: z.string().optional(), + timezone: z.string().default('local'), locale: z.string().optional(), + formatting: z.string().default('EEE d MMM t'), }); export type DateProvider = Provider; export interface DateOutput { + /** + * Current date/time as a formatted string. + */ + formatted: string; + /** * Current date/time as a JavaScript `Date` object. Uses `new Date()` under * the hood. @@ -73,8 +80,6 @@ export interface DateOutput { iso: string; } -// TODO: Organize provider-related types. -// TODO: Remove `toFormat` on date provider. Instead add `formatting` to the config. export async function createDateProvider( config: DateProviderConfig, ): Promise { @@ -89,13 +94,15 @@ export async function createDateProvider( ); function getDateValue() { - const date = new Date(); + const dateTime = DateTime.now().setZone(mergedConfig.timezone); return { - new: date, - now: date.getTime(), - iso: date.toISOString(), - formatted: 'todo', + new: dateTime.toJSDate(), + now: dateTime.toMillis(), + iso: dateTime.toISO()!, + formatted: dateTime.toFormat(mergedConfig.formatting, { + locale: mergedConfig.locale, + }), }; } From 7f61cf0c1a0c927b383b85bdc4c5780052e481ac Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Wed, 11 Sep 2024 02:30:50 +0800 Subject: [PATCH 109/138] feat: in default zebar styles, extend `body` to full viewport size --- packages/client-api/src/zebar-styles.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/client-api/src/zebar-styles.ts b/packages/client-api/src/zebar-styles.ts index bc1dfd54..f5765083 100644 --- a/packages/client-api/src/zebar-styles.ts +++ b/packages/client-api/src/zebar-styles.ts @@ -26,6 +26,15 @@ body { overscroll-behavior-x: none; } +/** + * Extend \`body\` to the full viewport. + */ +html, +body { + width: 100%; + height: 100%; +} + /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ /* Document From 65ad902feca5827da7a522a5fce7a834e16ed9ea Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Wed, 11 Sep 2024 04:04:55 +0800 Subject: [PATCH 110/138] feat: add markup and styling for vanilla example --- examples/boilerplates/solid-ts/src/index.tsx | 3 +- examples/boilerplates/solid-ts/vite.config.ts | 1 - examples/starter/styles.css | 103 +++++++++++ examples/starter/vanilla.html | 170 ++++++++++++++++-- examples/starter/vanilla.zebar.json | 6 +- 5 files changed, 261 insertions(+), 22 deletions(-) create mode 100644 examples/starter/styles.css diff --git a/examples/boilerplates/solid-ts/src/index.tsx b/examples/boilerplates/solid-ts/src/index.tsx index c19cd189..89ec60a4 100644 --- a/examples/boilerplates/solid-ts/src/index.tsx +++ b/examples/boilerplates/solid-ts/src/index.tsx @@ -3,9 +3,8 @@ import './index.css'; import { render } from 'solid-js/web'; import { createStore } from 'solid-js/store'; import { init } from 'zebar'; -import { createEffect } from 'solid-js'; -const zebarCtx = await init(); +const zebarCtx = await init({ includeDefaultCss: true }); const [cpu, battery, memory, weather] = await Promise.all([ zebarCtx.createProvider({ type: 'cpu' }), zebarCtx.createProvider({ type: 'battery' }), diff --git a/examples/boilerplates/solid-ts/vite.config.ts b/examples/boilerplates/solid-ts/vite.config.ts index 06d2b2e5..d3401ddc 100644 --- a/examples/boilerplates/solid-ts/vite.config.ts +++ b/examples/boilerplates/solid-ts/vite.config.ts @@ -3,7 +3,6 @@ import solidPlugin from 'vite-plugin-solid'; export default defineConfig({ plugins: [solidPlugin()], - server: { port: 3000 }, build: { target: 'esnext' }, base: './', }); diff --git a/examples/starter/styles.css b/examples/starter/styles.css new file mode 100644 index 00000000..c513b7b5 --- /dev/null +++ b/examples/starter/styles.css @@ -0,0 +1,103 @@ +/** + * Import the Nerdfonts icon font. + * Ref https://www.nerdfonts.com/cheat-sheet for a cheatsheet of available Nerdfonts icons. + */ +@import 'https://www.nerdfonts.com/assets/css/webfont.css'; + +i { + color: rgb(115 130 175 / 95%); + margin-right: 7px; +} + +body { + color: rgb(255 255 255 / 90%); + font-family: ui-monospace, monospace; + font-size: 12px; +} + +#root { + width: 100%; + height: 100%; + border-bottom: 1px solid rgb(255 255 255 / 5%); + background: linear-gradient(rgb(0 0 0 / 90%), rgb(5 2 20 / 85%)); +} + +.app { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + align-items: center; + height: 100%; + padding: 4px 24px; +} + +.left, +.center, +.right { + display: flex; + align-items: center; + + & > * { + margin-right: 20px; + } +} + +.center { + justify-self: center; +} + +.right { + justify-self: end; +} + +.logo { + margin-right: 20px; +} + +.workspaces { + display: flex; + align-items: center; + + .workspace { + background: rgb(255 255 255 / 5%); + margin-right: 4px; + padding: 4px 8px; + color: rgb(255 255 255 / 90%); + border: none; + border-radius: 2px; + cursor: pointer; + + &.displayed { + background: rgb(255 255 255 / 15%); + } + + &.focused, + &:hover { + background: rgb(75 115 255 / 50%); + } + } +} + +.binding-mode, +.tiling-direction { + background: rgb(255 255 255 / 15%); + color: rgb(255 255 255 / 90%); + border-radius: 2px; + padding: 4px 6px; + margin: 0; +} + +.tiling-direction { + border: 0; + cursor: pointer; +} + +.high-usage { + color: #900029; +} + +.charging-icon { + position: absolute; + font-size: 11px; + left: 7px; + top: 2px; +} diff --git a/examples/starter/vanilla.html b/examples/starter/vanilla.html index cb79ec27..016535fa 100644 --- a/examples/starter/vanilla.html +++ b/examples/starter/vanilla.html @@ -1,21 +1,159 @@ - - -

fjdisa

- + + + + +
+ + diff --git a/examples/starter/vanilla.zebar.json b/examples/starter/vanilla.zebar.json index 7a5584c5..390b8f67 100644 --- a/examples/starter/vanilla.zebar.json +++ b/examples/starter/vanilla.zebar.json @@ -10,10 +10,10 @@ "placements": [ { "anchor": "top_left", - "offsetX": "20px", - "offsetY": "20px", + "offsetX": "0px", + "offsetY": "0px", "width": "100%", - "height": "30px", + "height": "40px", "monitorSelection": { "type": "all" } From 969f61c32b07986c183f824d22828d48b0301fb9 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Wed, 11 Sep 2024 16:49:51 +0800 Subject: [PATCH 111/138] feat: remove default zebar styles; add modern normalize stylesheet --- .../boilerplates/react-buildless/index.html | 1 + examples/boilerplates/solid-ts/src/index.tsx | 2 +- examples/normalize.css | 204 +++++++++ examples/starter/vanilla.html | 2 +- packages/client-api/src/init.ts | 27 +- packages/client-api/src/zebar-styles.ts | 389 ------------------ 6 files changed, 208 insertions(+), 417 deletions(-) create mode 100644 examples/normalize.css delete mode 100644 packages/client-api/src/zebar-styles.ts diff --git a/examples/boilerplates/react-buildless/index.html b/examples/boilerplates/react-buildless/index.html index ed168e57..dc1a3530 100644 --- a/examples/boilerplates/react-buildless/index.html +++ b/examples/boilerplates/react-buildless/index.html @@ -20,6 +20,7 @@ import { init } from 'zebar'; const zebarCtx = await init(); + const [cpu, battery, memory, weather] = await Promise.all([ zebarCtx.createProvider({ type: 'cpu' }), zebarCtx.createProvider({ type: 'battery' }), diff --git a/examples/boilerplates/solid-ts/src/index.tsx b/examples/boilerplates/solid-ts/src/index.tsx index 89ec60a4..0169b3a4 100644 --- a/examples/boilerplates/solid-ts/src/index.tsx +++ b/examples/boilerplates/solid-ts/src/index.tsx @@ -4,7 +4,7 @@ import { render } from 'solid-js/web'; import { createStore } from 'solid-js/store'; import { init } from 'zebar'; -const zebarCtx = await init({ includeDefaultCss: true }); +const zebarCtx = await init(); const [cpu, battery, memory, weather] = await Promise.all([ zebarCtx.createProvider({ type: 'cpu' }), zebarCtx.createProvider({ type: 'battery' }), diff --git a/examples/normalize.css b/examples/normalize.css new file mode 100644 index 00000000..2204e781 --- /dev/null +++ b/examples/normalize.css @@ -0,0 +1,204 @@ +/** + * Base CSS styles for better consistency across platforms. + * Yoinked from: https://github.com/sindresorhus/modern-normalize + */ + +/* +Document +======== +*/ + +/** +Use a better box model (opinionated). +*/ + +*, +::before, +::after { + box-sizing: border-box; +} + +html { + /* Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) */ + font-family: system-ui, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, + 'Apple Color Emoji', 'Segoe UI Emoji'; + line-height: 1.15; /* 1. Correct the line height in all browsers. */ + -webkit-text-size-adjust: 100%; /* 2. Prevent adjustments of font size after orientation changes in iOS. */ + tab-size: 4; /* 3. Use a more readable tab size (opinionated). */ +} + +/* +Sections +======== +*/ + +body { + margin: 0; /* Remove the margin in all browsers. */ +} + +/* +Text-level semantics +==================== +*/ + +/** +Add the correct font weight in Chrome and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/** +1. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) +2. Correct the odd 'em' font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', + Menlo, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/** +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/** +Prevent 'sub' and 'sup' elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +Tabular data +============ +*/ + +/** +Correct table border color inheritance in Chrome and Safari. (https://issues.chromium.org/issues/40615503, https://bugs.webkit.org/show_bug.cgi?id=195016) +*/ + +table { + border-color: currentcolor; +} + +/* +Forms +===== +*/ + +/** +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ +} + +/** +Correct the inability to style clickable types in iOS and Safari. +*/ + +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; +} + +/** +Remove the padding so developers are not caught out when they zero out 'fieldset' elements in all browsers. +*/ + +legend { + padding: 0; +} + +/** +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/** +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/** +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/** +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to 'inherit' in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* +Interactive +=========== +*/ + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} diff --git a/examples/starter/vanilla.html b/examples/starter/vanilla.html index 016535fa..dd6cd3af 100644 --- a/examples/starter/vanilla.html +++ b/examples/starter/vanilla.html @@ -25,7 +25,7 @@ import { createRoot } from 'react-dom/client'; import { init } from 'zebar'; - const zebarCtx = await init({ includeDefaultCss: true }); + const zebarCtx = await init(); const [cpu, date, battery, memory, weather] = await Promise.all([ zebarCtx.createProvider({ type: 'cpu' }), diff --git a/packages/client-api/src/init.ts b/packages/client-api/src/init.ts index 36c9b257..726f2240 100644 --- a/packages/client-api/src/init.ts +++ b/packages/client-api/src/init.ts @@ -10,30 +10,13 @@ import { import { createLogger } from '~/utils'; import type { ZebarContext } from './zebar-context.model'; import { createProvider } from './providers'; -import { zebarStyles } from './zebar-styles'; const logger = createLogger('init-window'); -export interface ZebarInitOptions { - /** - * Whether to add basic default CSS in the window. - * - * Includes: - * - Setting box-sizing to border-box - * - Disabling overscroll - * - [normalize.css](https://github.com/necolas/normalize.css) - * - * Defaults to `true`. - */ - includeDefaultCss?: boolean; -} - /** * Handles initialization. */ -export async function init( - options?: ZebarInitOptions, -): Promise { +export async function init(): Promise { try { const currentWindow = getCurrentWindow(); @@ -41,14 +24,6 @@ export async function init( window.__ZEBAR_INITIAL_STATE ?? (await getWindowState(currentWindow.label)); - // Load default CSS unless explicitly disabled. - if (options?.includeDefaultCss !== false) { - const styleElement = document.createElement('style'); - styleElement.setAttribute('data-zebar', ''); - styleElement.innerHTML = zebarStyles; - document.head.appendChild(styleElement); - } - // @ts-ignore - TODO return { openWindow: async (configPath: string) => { diff --git a/packages/client-api/src/zebar-styles.ts b/packages/client-api/src/zebar-styles.ts deleted file mode 100644 index f5765083..00000000 --- a/packages/client-api/src/zebar-styles.ts +++ /dev/null @@ -1,389 +0,0 @@ -import type { ZebarInitOptions } from './init'; - -/** - * Default CSS if {@link ZebarInitOptions.includeDefaultCss} is enabled. - */ -export const zebarStyles = ` -html { - box-sizing: border-box; -} - -/** - * Set default \`box-sizing\` for all elements to border-box. - */ -*, -*:before, -*:after { - box-sizing: inherit; -} - -/** - * Disable overscroll. - */ -body { - overflow: hidden; - overscroll-behavior-y: none; - overscroll-behavior-x: none; -} - -/** - * Extend \`body\` to the full viewport. - */ -html, -body { - width: 100%; - height: 100%; -} - -/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ - -/* Document - ========================================================================== */ - -/** - * 1. Correct the line height in all browsers. - * 2. Prevent adjustments of font size after orientation changes in iOS. - */ - -html { - line-height: 1.15; /* 1 */ - -webkit-text-size-adjust: 100%; /* 2 */ -} - -/* Sections - ========================================================================== */ - -/** - * Remove the margin in all browsers. - */ - -body { - margin: 0; -} - -/** - * Render the \`main\` element consistently in IE. - */ - -main { - display: block; -} - -/** - * Correct the font size and margin on \`h1\` elements within \`section\` and - * \`article\` contexts in Chrome, Firefox, and Safari. - */ - -h1 { - font-size: 2em; - margin: 0.67em 0; -} - -/* Grouping content - ========================================================================== */ - -/** - * 1. Add the correct box sizing in Firefox. - * 2. Show the overflow in Edge and IE. - */ - -hr { - box-sizing: content-box; /* 1 */ - height: 0; /* 1 */ - overflow: visible; /* 2 */ -} - -/** - * 1. Correct the inheritance and scaling of font size in all browsers. - * 2. Correct the odd \`em\` font sizing in all browsers. - */ - -pre { - font-family: monospace, monospace; /* 1 */ - font-size: 1em; /* 2 */ -} - -/* Text-level semantics - ========================================================================== */ - -/** - * Remove the gray background on active links in IE 10. - */ - -a { - background-color: transparent; -} - -/** - * 1. Remove the bottom border in Chrome 57- - * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. - */ - -abbr[title] { - border-bottom: none; /* 1 */ - text-decoration: underline; /* 2 */ - text-decoration: underline dotted; /* 2 */ -} - -/** - * Add the correct font weight in Chrome, Edge, and Safari. - */ - -b, -strong { - font-weight: bolder; -} - -/** - * 1. Correct the inheritance and scaling of font size in all browsers. - * 2. Correct the odd \`em\` font sizing in all browsers. - */ - -code, -kbd, -samp { - font-family: monospace, monospace; /* 1 */ - font-size: 1em; /* 2 */ -} - -/** - * Add the correct font size in all browsers. - */ - -small { - font-size: 80%; -} - -/** - * Prevent \`sub\` and \`sup\` elements from affecting the line height in - * all browsers. - */ - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sub { - bottom: -0.25em; -} - -sup { - top: -0.5em; -} - -/* Embedded content - ========================================================================== */ - -/** - * Remove the border on images inside links in IE 10. - */ - -img { - border-style: none; -} - -/* Forms - ========================================================================== */ - -/** - * 1. Change the font styles in all browsers. - * 2. Remove the margin in Firefox and Safari. - */ - -button, -input, -optgroup, -select, -textarea { - font-family: inherit; /* 1 */ - font-size: 100%; /* 1 */ - line-height: 1.15; /* 1 */ - margin: 0; /* 2 */ -} - -/** - * Show the overflow in IE. - * 1. Show the overflow in Edge. - */ - -button, -input { - /* 1 */ - overflow: visible; -} - -/** - * Remove the inheritance of text transform in Edge, Firefox, and IE. - * 1. Remove the inheritance of text transform in Firefox. - */ - -button, -select { - /* 1 */ - text-transform: none; -} - -/** - * Correct the inability to style clickable types in iOS and Safari. - */ - -button, -[type='button'], -[type='reset'], -[type='submit'] { - -webkit-appearance: button; -} - -/** - * Remove the inner border and padding in Firefox. - */ - -button::-moz-focus-inner, -[type='button']::-moz-focus-inner, -[type='reset']::-moz-focus-inner, -[type='submit']::-moz-focus-inner { - border-style: none; - padding: 0; -} - -/** - * Restore the focus styles unset by the previous rule. - */ - -button:-moz-focusring, -[type='button']:-moz-focusring, -[type='reset']:-moz-focusring, -[type='submit']:-moz-focusring { - outline: 1px dotted ButtonText; -} - -/** - * Correct the padding in Firefox. - */ - -fieldset { - padding: 0.35em 0.75em 0.625em; -} - -/** - * 1. Correct the text wrapping in Edge and IE. - * 2. Correct the color inheritance from \`fieldset\` elements in IE. - * 3. Remove the padding so developers are not caught out when they zero out - * \`fieldset\` elements in all browsers. - */ - -legend { - box-sizing: border-box; /* 1 */ - color: inherit; /* 2 */ - display: table; /* 1 */ - max-width: 100%; /* 1 */ - padding: 0; /* 3 */ - white-space: normal; /* 1 */ -} - -/** - * Add the correct vertical alignment in Chrome, Firefox, and Opera. - */ - -progress { - vertical-align: baseline; -} - -/** - * Remove the default vertical scrollbar in IE 10+. - */ - -textarea { - overflow: auto; -} - -/** - * 1. Add the correct box sizing in IE 10. - * 2. Remove the padding in IE 10. - */ - -[type='checkbox'], -[type='radio'] { - box-sizing: border-box; /* 1 */ - padding: 0; /* 2 */ -} - -/** - * Correct the cursor style of increment and decrement buttons in Chrome. - */ - -[type='number']::-webkit-inner-spin-button, -[type='number']::-webkit-outer-spin-button { - height: auto; -} - -/** - * 1. Correct the odd appearance in Chrome and Safari. - * 2. Correct the outline style in Safari. - */ - -[type='search'] { - -webkit-appearance: textfield; /* 1 */ - outline-offset: -2px; /* 2 */ -} - -/** - * Remove the inner padding in Chrome and Safari on macOS. - */ - -[type='search']::-webkit-search-decoration { - -webkit-appearance: none; -} - -/** - * 1. Correct the inability to style clickable types in iOS and Safari. - * 2. Change font properties to \`inherit\` in Safari. - */ - -::-webkit-file-upload-button { - -webkit-appearance: button; /* 1 */ - font: inherit; /* 2 */ -} - -/* Interactive - ========================================================================== */ - -/* - * Add the correct display in Edge, IE 10+, and Firefox. - */ - -details { - display: block; -} - -/* - * Add the correct display in all browsers. - */ - -summary { - display: list-item; -} - -/* Misc - ========================================================================== */ - -/** - * Add the correct display in IE 10+. - */ - -template { - display: none; -} - -/** - * Add the correct display in IE 10. - */ - -[hidden] { - display: none; -} -`; From 1be2c6095ce164b48343e295f1bb7542c49dc60e Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Wed, 11 Sep 2024 16:55:23 +0800 Subject: [PATCH 112/138] feat: add glazewm and komorebi samples --- examples/starter/styles.css | 68 ++++---- examples/starter/vanilla.html | 1 + examples/starter/with-glazewm.html | 190 ++++++++++++++++++++-- examples/starter/with-glazewm.zebar.json | 6 +- examples/starter/with-komorebi.html | 184 +++++++++++++++++++-- examples/starter/with-komorebi.zebar.json | 6 +- 6 files changed, 391 insertions(+), 64 deletions(-) diff --git a/examples/starter/styles.css b/examples/starter/styles.css index c513b7b5..a7ef7efc 100644 --- a/examples/starter/styles.css +++ b/examples/starter/styles.css @@ -15,9 +15,13 @@ body { font-size: 12px; } +html, +body, #root { - width: 100%; height: 100%; +} + +#root { border-bottom: 1px solid rgb(255 255 255 / 5%); background: linear-gradient(rgb(0 0 0 / 90%), rgb(5 2 20 / 85%)); } @@ -27,7 +31,7 @@ body { grid-template-columns: 1fr 1fr 1fr; align-items: center; height: 100%; - padding: 4px 24px; + padding: 4px 1.5vw; } .left, @@ -35,10 +39,6 @@ body { .right { display: flex; align-items: center; - - & > * { - margin-right: 20px; - } } .center { @@ -49,31 +49,36 @@ body { justify-self: end; } -.logo { +.logo, +.binding-mode, +.tiling-direction, +.memory, +.cpu, +.battery { margin-right: 20px; } .workspaces { display: flex; align-items: center; +} + +.workspace { + background: rgb(255 255 255 / 5%); + margin-right: 4px; + padding: 4px 8px; + color: rgb(255 255 255 / 90%); + border: none; + border-radius: 2px; + cursor: pointer; - .workspace { - background: rgb(255 255 255 / 5%); - margin-right: 4px; - padding: 4px 8px; - color: rgb(255 255 255 / 90%); - border: none; - border-radius: 2px; - cursor: pointer; - - &.displayed { - background: rgb(255 255 255 / 15%); - } - - &.focused, - &:hover { - background: rgb(75 115 255 / 50%); - } + &.displayed { + background: rgb(255 255 255 / 15%); + } + + &.focused, + &:hover { + background: rgb(75 115 255 / 50%); } } @@ -82,20 +87,21 @@ body { background: rgb(255 255 255 / 15%); color: rgb(255 255 255 / 90%); border-radius: 2px; - padding: 4px 6px; - margin: 0; -} - -.tiling-direction { + line-height: 1; + padding: 4px 8px; border: 0; cursor: pointer; } -.high-usage { +.binding-mode { + margin-right: 4px; +} + +.cpu .high-usage { color: #900029; } -.charging-icon { +.battery .charging-icon { position: absolute; font-size: 11px; left: 7px; diff --git a/examples/starter/vanilla.html b/examples/starter/vanilla.html index dd6cd3af..db41417a 100644 --- a/examples/starter/vanilla.html +++ b/examples/starter/vanilla.html @@ -4,6 +4,7 @@ + diff --git a/examples/starter/with-glazewm.html b/examples/starter/with-glazewm.html index cb79ec27..a04d635e 100644 --- a/examples/starter/with-glazewm.html +++ b/examples/starter/with-glazewm.html @@ -1,21 +1,183 @@ - + + + + + + + + + + + + -

fjdisa

- diff --git a/examples/starter/with-glazewm.zebar.json b/examples/starter/with-glazewm.zebar.json index 0ce8cc53..465424d7 100644 --- a/examples/starter/with-glazewm.zebar.json +++ b/examples/starter/with-glazewm.zebar.json @@ -10,10 +10,10 @@ "placements": [ { "anchor": "top_left", - "offsetX": "20px", - "offsetY": "20px", + "offsetX": "0px", + "offsetY": "0px", "width": "100%", - "height": "30px", + "height": "40px", "monitorSelection": { "type": "all" } diff --git a/examples/starter/with-komorebi.html b/examples/starter/with-komorebi.html index 495ff786..39264355 100644 --- a/examples/starter/with-komorebi.html +++ b/examples/starter/with-komorebi.html @@ -1,20 +1,178 @@ - + + + + + + + + + + + + - diff --git a/examples/starter/with-komorebi.zebar.json b/examples/starter/with-komorebi.zebar.json index 1cdeb957..92316f36 100644 --- a/examples/starter/with-komorebi.zebar.json +++ b/examples/starter/with-komorebi.zebar.json @@ -10,10 +10,10 @@ "placements": [ { "anchor": "top_left", - "offsetX": "20px", - "offsetY": "20px", + "offsetX": "0px", + "offsetY": "0px", "width": "100%", - "height": "30px", + "height": "40px", "monitorSelection": { "type": "all" } From 64b59680521995628782f9b82fadbfe76bc2b0d2 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Wed, 11 Sep 2024 17:22:26 +0800 Subject: [PATCH 113/138] feat: remove `toggleTilingDirection` and `focusWorkspace`; add onclick events for glazewm in sample --- examples/starter/with-glazewm.html | 55 ++++++++++++------- .../glazewm/create-glazewm-provider.ts | 32 ++++++----- 2 files changed, 53 insertions(+), 34 deletions(-) diff --git a/examples/starter/with-glazewm.html b/examples/starter/with-glazewm.html index a04d635e..7c10029c 100644 --- a/examples/starter/with-glazewm.html +++ b/examples/starter/with-glazewm.html @@ -118,30 +118,47 @@
-
- {outputs.glazewm.currentWorkspaces.map(workspace => ( - - ))} -
+ {outputs.glazewm && ( +
+ {outputs.glazewm.currentWorkspaces.map(workspace => ( + + ))} +
+ )}
{outputs.date.formatted}
- {outputs.glazewm.bindingModes.map(bindingMode => ( - - ))} - - + {outputs.glazewm && ( + <> + {outputs.glazewm.bindingModes.map(bindingMode => ( + + ))} + + + + )}
diff --git a/packages/client-api/src/providers/glazewm/create-glazewm-provider.ts b/packages/client-api/src/providers/glazewm/create-glazewm-provider.ts index 2592139d..32e17122 100644 --- a/packages/client-api/src/providers/glazewm/create-glazewm-provider.ts +++ b/packages/client-api/src/providers/glazewm/create-glazewm-provider.ts @@ -8,6 +8,7 @@ import { type FocusChangedEvent, type FocusedContainerMovedEvent, type Monitor, + type RunCommandResponse, type TilingDirectionChangedEvent, type Workspace, type WorkspaceActivatedEvent, @@ -88,14 +89,17 @@ export interface GlazeWmOutput { bindingModes: BindingModeConfig[]; /** - * Focus a workspace by name. + * Invokes a WM command (e.g. `"focus --workspace 1"`). + * + * @param command WM command to run (e.g. `"focus --workspace 1"`). + * @param subjectContainerId (optional) ID of container to use as subject. + * If not provided, this defaults to the currently focused container. + * @throws If command fails. */ - focusWorkspace(name: string): void; - - /** - * Toggle tiling direction. - */ - toggleTilingDirection(): void; + runCommand( + command: string, + subjectContainerId?: string, + ): Promise; } export async function createGlazeWmProvider( @@ -172,12 +176,11 @@ export async function createGlazeWmProvider( queue.output(state); } - function focusWorkspace(name: string) { - client.runCommand(`focus --workspace ${name}`); - } - - function toggleTilingDirection() { - client.runCommand('toggle-tiling-direction'); + function runCommand( + command: string, + subjectContainerId?: string, + ): Promise { + return client.runCommand(command, subjectContainerId); } async function getInitialState() { @@ -190,8 +193,7 @@ export async function createGlazeWmProvider( focusedContainer, tilingDirection, bindingModes, - focusWorkspace, - toggleTilingDirection, + runCommand, }; } From b4f2d928ec17ff79994ba19006a8b9cd10d8f06b Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Wed, 11 Sep 2024 17:25:48 +0800 Subject: [PATCH 114/138] feat: revert `hasBattery` property on battery provider --- examples/starter/vanilla.html | 2 +- examples/starter/with-glazewm.html | 2 +- examples/starter/with-komorebi.html | 25 +++++---- .../battery/create-battery-provider.ts | 9 ++-- .../desktop/src/providers/battery/provider.rs | 53 +++++++------------ .../src/providers/battery/variables.rs | 9 ++-- 6 files changed, 43 insertions(+), 57 deletions(-) diff --git a/examples/starter/vanilla.html b/examples/starter/vanilla.html index db41417a..0faa475d 100644 --- a/examples/starter/vanilla.html +++ b/examples/starter/vanilla.html @@ -136,7 +136,7 @@
- {outputs.battery.hasBattery && ( + {outputs.battery && (
{/* Show icon for whether battery is charging. */} {outputs.battery.isCharging && ( diff --git a/examples/starter/with-glazewm.html b/examples/starter/with-glazewm.html index 7c10029c..a53f26b2 100644 --- a/examples/starter/with-glazewm.html +++ b/examples/starter/with-glazewm.html @@ -176,7 +176,7 @@
- {outputs.battery.hasBattery && ( + {outputs.battery && (
{/* Show icon for whether battery is charging. */} {outputs.battery.isCharging && ( diff --git a/examples/starter/with-komorebi.html b/examples/starter/with-komorebi.html index 39264355..370b6bc9 100644 --- a/examples/starter/with-komorebi.html +++ b/examples/starter/with-komorebi.html @@ -123,16 +123,19 @@
-
- {outputs.komorebi.currentWorkspaces.map(workspace => ( - - ))} -
+ + {outputs.komorebi && ( +
+ {outputs.komorebi.currentWorkspaces.map(workspace => ( + + ))} +
+ )}
{outputs.date.formatted}
@@ -154,7 +157,7 @@
- {outputs.battery.hasBattery && ( + {outputs.battery && (
{/* Show icon for whether battery is charging. */} {outputs.battery.isCharging && ( diff --git a/packages/client-api/src/providers/battery/create-battery-provider.ts b/packages/client-api/src/providers/battery/create-battery-provider.ts index 0013129c..d1b43807 100644 --- a/packages/client-api/src/providers/battery/create-battery-provider.ts +++ b/packages/client-api/src/providers/battery/create-battery-provider.ts @@ -26,11 +26,10 @@ export type BatteryProvider = Provider< >; export interface BatteryOutput { - hasBattery: boolean; - chargePercent: number | null; - cycleCount: number | null; - healthPercent: number | null; - powerConsumption: number | null; + chargePercent: number; + cycleCount: number; + healthPercent: number; + powerConsumption: number; state: 'discharging' | 'charging' | 'full' | 'empty' | 'unknown'; isCharging: boolean; timeTillEmpty: number | null; diff --git a/packages/desktop/src/providers/battery/provider.rs b/packages/desktop/src/providers/battery/provider.rs index 4f5f7c26..d716a77c 100644 --- a/packages/desktop/src/providers/battery/provider.rs +++ b/packages/desktop/src/providers/battery/provider.rs @@ -1,3 +1,4 @@ +use anyhow::Context; use starship_battery::{ units::{ electric_potential::volt, power::watt, ratio::percent, @@ -26,40 +27,24 @@ impl BatteryProvider { let battery = Manager::new()? .batteries() .and_then(|mut batteries| batteries.nth(0).transpose()) - .unwrap_or(None); - - let output = match battery { - None => BatteryOutput { - has_battery: false, - charge_percent: None, - health_percent: None, - state: "unknown".to_string(), - is_charging: false, - time_till_full: None, - time_till_empty: None, - power_consumption: None, - voltage: None, - cycle_count: None, - }, - Some(battery) => BatteryOutput { - has_battery: true, - charge_percent: Some(battery.state_of_charge().get::()), - health_percent: Some(battery.state_of_health().get::()), - state: battery.state().to_string(), - is_charging: battery.state() == State::Charging, - time_till_full: battery - .time_to_full() - .map(|time| time.get::()), - time_till_empty: battery - .time_to_empty() - .map(|time| time.get::()), - power_consumption: Some(battery.energy_rate().get::()), - voltage: Some(battery.voltage().get::()), - cycle_count: battery.cycle_count(), - }, - }; - - Ok(ProviderOutput::Battery(output)) + .unwrap_or(None) + .context("No battery found.")?; + + Ok(ProviderOutput::Battery(BatteryOutput { + charge_percent: battery.state_of_charge().get::(), + health_percent: battery.state_of_health().get::(), + state: battery.state().to_string(), + is_charging: battery.state() == State::Charging, + time_till_full: battery + .time_to_full() + .map(|time| time.get::()), + time_till_empty: battery + .time_to_empty() + .map(|time| time.get::()), + power_consumption: battery.energy_rate().get::(), + voltage: battery.voltage().get::(), + cycle_count: battery.cycle_count(), + })) } } diff --git a/packages/desktop/src/providers/battery/variables.rs b/packages/desktop/src/providers/battery/variables.rs index 186cd995..dbb4cf57 100644 --- a/packages/desktop/src/providers/battery/variables.rs +++ b/packages/desktop/src/providers/battery/variables.rs @@ -3,14 +3,13 @@ use serde::Serialize; #[derive(Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct BatteryOutput { - pub has_battery: bool, - pub charge_percent: Option, - pub health_percent: Option, + pub charge_percent: f32, + pub health_percent: f32, pub state: String, pub is_charging: bool, pub time_till_full: Option, pub time_till_empty: Option, - pub power_consumption: Option, - pub voltage: Option, + pub power_consumption: f32, + pub voltage: f32, pub cycle_count: Option, } From 434a536118e43024b5afe7a183d062fef7fe4324 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Wed, 11 Sep 2024 17:34:20 +0800 Subject: [PATCH 115/138] fix: prevent crash if failing to parse wifi hotspot ssid/signal strength --- .../src/providers/network/wifi_hotspot.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/desktop/src/providers/network/wifi_hotspot.rs b/packages/desktop/src/providers/network/wifi_hotspot.rs index 673e2742..de3143f0 100644 --- a/packages/desktop/src/providers/network/wifi_hotspot.rs +++ b/packages/desktop/src/providers/network/wifi_hotspot.rs @@ -38,13 +38,21 @@ pub fn default_gateway_wifi() -> anyhow::Result { let ssid = ssid_match .captures(&output) - .map(|m| m.get(1).unwrap().as_str().to_string()); - let signal_str: Option<&str> = signal_match + .context("Failed to parse WiFi hotspot SSID.")? + .get(1) + .map(|s| s.as_str().to_string()); + + let signal_str = signal_match .captures(&output) - .map(|m| m.get(1).unwrap().as_str()); - let signal: Option = signal_str + .context("Failed to parse WiFi hotspot signal strength.")? + .get(1) + .map(|s| s.as_str()); + + let signal = signal_str .and_then(|s| signal_strip.captures(s)) - .map(|m| m.get(1).unwrap().as_str().parse().unwrap()); + .context("Failed to parse WiFi hotspot signal strength.")? + .get(1) + .and_then(|s| s.as_str().parse().ok()); return Ok(WifiHotstop { ssid, From 5b411e292b27a156fd0a5beaa471f6d604cc26bd Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Wed, 11 Sep 2024 17:54:35 +0800 Subject: [PATCH 116/138] feat: show network status in examples --- examples/starter/styles.css | 1 + examples/starter/vanilla.html | 75 +++++++++++++++++++++++------ examples/starter/with-glazewm.html | 56 ++++++++++++++++++--- examples/starter/with-komorebi.html | 56 ++++++++++++++++++--- 4 files changed, 160 insertions(+), 28 deletions(-) diff --git a/examples/starter/styles.css b/examples/starter/styles.css index a7ef7efc..a42fb93a 100644 --- a/examples/starter/styles.css +++ b/examples/starter/styles.css @@ -52,6 +52,7 @@ body, .logo, .binding-mode, .tiling-direction, +.network, .memory, .cpu, .battery { diff --git a/examples/starter/vanilla.html b/examples/starter/vanilla.html index 0faa475d..825c3bad 100644 --- a/examples/starter/vanilla.html +++ b/examples/starter/vanilla.html @@ -28,21 +28,24 @@ const zebarCtx = await init(); - const [cpu, date, battery, memory, weather] = await Promise.all([ - zebarCtx.createProvider({ type: 'cpu' }), - zebarCtx.createProvider({ - type: 'date', - formatting: 'EEE d MMM t', - }), - zebarCtx.createProvider({ type: 'battery' }), - zebarCtx.createProvider({ type: 'memory' }), - zebarCtx.createProvider({ type: 'weather' }), - ]); + const [network, cpu, date, battery, memory, weather] = + await Promise.all([ + zebarCtx.createProvider({ type: 'network' }), + zebarCtx.createProvider({ type: 'cpu' }), + zebarCtx.createProvider({ + type: 'date', + formatting: 'EEE d MMM t', + }), + zebarCtx.createProvider({ type: 'battery' }), + zebarCtx.createProvider({ type: 'memory' }), + zebarCtx.createProvider({ type: 'weather' }), + ]); createRoot(document.getElementById('root')).render(); function App() { const [outputs, setOutputs] = useState({ + network: network.output, cpu: cpu.output, date: date.output, battery: battery.output, @@ -51,6 +54,9 @@ }); useEffect(() => { + network.onOutput(network => + setOutputs(outputs => ({ ...outputs, network })), + ); cpu.onOutput(cpu => setOutputs(outputs => ({ ...outputs, cpu })), ); @@ -68,6 +74,36 @@ ); }, []); + // Get icon to show for current network status. + function getNetworkIcon() { + switch (outputs.network.defaultInterface?.type) { + case 'ethernet': + return ; + case 'wifi': + if (outputs.network.defaultGateway?.signalStrength >= 80) { + return ; + } else if ( + outputs.network.defaultGateway?.signalStrength >= 65 + ) { + return ; + } else if ( + outputs.network.defaultGateway?.signalStrength >= 40 + ) { + return ; + } else if ( + outputs.network.defaultGateway?.signalStrength >= 25 + ) { + return ; + } else { + return ; + } + default: + return ( + + ); + } + } + // Get icon to show for how much of the battery is charged. function getBatteryIcon() { if (outputs.battery.chargePercent > 90) @@ -81,7 +117,7 @@ return ; } - // Get icon to show for how much of the battery is charged. + // Get icon to show for current weather status. function getWeatherIcon() { switch (outputs.weather.status) { case 'clear_day': @@ -120,6 +156,13 @@
{outputs.date.formatted}
+ {outputs.network && ( +
+ {getNetworkIcon()} + {outputs.network.defaultGateway?.ssid} +
+ )} +
{Math.round(outputs.memory.usage)}% @@ -147,10 +190,12 @@
)} -
- {getWeatherIcon()} - {Math.round(outputs.weather.celsiusTemp)}°C -
+ {outputs.weather && ( +
+ {getWeatherIcon()} + {Math.round(outputs.weather.celsiusTemp)}°C +
+ )}
); diff --git a/examples/starter/with-glazewm.html b/examples/starter/with-glazewm.html index a53f26b2..33e3c3bc 100644 --- a/examples/starter/with-glazewm.html +++ b/examples/starter/with-glazewm.html @@ -28,8 +28,9 @@ const zebarCtx = await init(); - const [glazewm, cpu, date, battery, memory, weather] = + const [network, glazewm, cpu, date, battery, memory, weather] = await Promise.all([ + zebarCtx.createProvider({ type: 'network' }), zebarCtx.createProvider({ type: 'glazewm' }), zebarCtx.createProvider({ type: 'cpu' }), zebarCtx.createProvider({ @@ -45,6 +46,7 @@ function App() { const [outputs, setOutputs] = useState({ + network: network.output, glazewm: glazewm.output, cpu: cpu.output, date: date.output, @@ -54,6 +56,9 @@ }); useEffect(() => { + network.onOutput(network => + setOutputs(outputs => ({ ...outputs, network })), + ); glazewm.onOutput(glazewm => setOutputs(outputs => ({ ...outputs, glazewm })), ); @@ -71,6 +76,36 @@ ); }, []); + // Get icon to show for current network status. + function getNetworkIcon() { + switch (outputs.network.defaultInterface?.type) { + case 'ethernet': + return ; + case 'wifi': + if (outputs.network.defaultGateway?.signalStrength >= 80) { + return ; + } else if ( + outputs.network.defaultGateway?.signalStrength >= 65 + ) { + return ; + } else if ( + outputs.network.defaultGateway?.signalStrength >= 40 + ) { + return ; + } else if ( + outputs.network.defaultGateway?.signalStrength >= 25 + ) { + return ; + } else { + return ; + } + default: + return ( + + ); + } + } + // Get icon to show for how much of the battery is charged. function getBatteryIcon() { if (outputs.battery.chargePercent > 90) @@ -84,7 +119,7 @@ return ; } - // Get icon to show for how much of the battery is charged. + // Get icon to show for current weather status. function getWeatherIcon() { switch (outputs.weather.status) { case 'clear_day': @@ -160,6 +195,13 @@ )} + {outputs.network && ( +
+ {getNetworkIcon()} + {outputs.network.defaultGateway?.ssid} +
+ )} +
{Math.round(outputs.memory.usage)}% @@ -187,10 +229,12 @@
)} -
- {getWeatherIcon()} - {Math.round(outputs.weather.celsiusTemp)}°C -
+ {outputs.weather && ( +
+ {getWeatherIcon()} + {Math.round(outputs.weather.celsiusTemp)}°C +
+ )}
); diff --git a/examples/starter/with-komorebi.html b/examples/starter/with-komorebi.html index 370b6bc9..273f83be 100644 --- a/examples/starter/with-komorebi.html +++ b/examples/starter/with-komorebi.html @@ -28,8 +28,9 @@ const zebarCtx = await init(); - const [komorebi, cpu, date, battery, memory, weather] = + const [network, komorebi, cpu, date, battery, memory, weather] = await Promise.all([ + zebarCtx.createProvider({ type: 'network' }), zebarCtx.createProvider({ type: 'komorebi' }), zebarCtx.createProvider({ type: 'cpu' }), zebarCtx.createProvider({ @@ -45,6 +46,7 @@ function App() { const [outputs, setOutputs] = useState({ + network: network.output, komorebi: komorebi.output, cpu: cpu.output, date: date.output, @@ -54,6 +56,9 @@ }); useEffect(() => { + network.onOutput(network => + setOutputs(outputs => ({ ...outputs, network })), + ); komorebi.onOutput(komorebi => setOutputs(outputs => ({ ...outputs, komorebi })), ); @@ -74,7 +79,35 @@ ); }, []); - console.log('outputs.komorebi', outputs.komorebi); + // Get icon to show for current network status. + function getNetworkIcon() { + switch (outputs.network.defaultInterface?.type) { + case 'ethernet': + return ; + case 'wifi': + if (outputs.network.defaultGateway?.signalStrength >= 80) { + return ; + } else if ( + outputs.network.defaultGateway?.signalStrength >= 65 + ) { + return ; + } else if ( + outputs.network.defaultGateway?.signalStrength >= 40 + ) { + return ; + } else if ( + outputs.network.defaultGateway?.signalStrength >= 25 + ) { + return ; + } else { + return ; + } + default: + return ( + + ); + } + } // Get icon to show for how much of the battery is charged. function getBatteryIcon() { @@ -89,7 +122,7 @@ return ; } - // Get icon to show for how much of the battery is charged. + // Get icon to show for current weather status. function getWeatherIcon() { switch (outputs.weather.status) { case 'clear_day': @@ -141,6 +174,13 @@
{outputs.date.formatted}
+ {outputs.network && ( +
+ {getNetworkIcon()} + {outputs.network.defaultGateway?.ssid} +
+ )} +
{Math.round(outputs.memory.usage)}% @@ -168,10 +208,12 @@
)} -
- {getWeatherIcon()} - {Math.round(outputs.weather.celsiusTemp)}°C -
+ {outputs.weather && ( +
+ {getWeatherIcon()} + {Math.round(outputs.weather.celsiusTemp)}°C +
+ )}
); From 6813c300d392042bb53a29e3fc7701cff919f94d Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Wed, 11 Sep 2024 18:35:24 +0800 Subject: [PATCH 117/138] feat: improve boilerplates --- .../boilerplates/react-buildless/index.html | 46 +++++++++++++------ .../react-buildless/my-window.zebar.json | 2 +- .../boilerplates/react-buildless/styles.css | 23 ++++++++++ examples/boilerplates/solid-ts/README.md | 20 ++------ examples/boilerplates/solid-ts/index.html | 3 +- .../solid-ts/my-window.zebar.json | 6 +-- examples/boilerplates/solid-ts/src/index.css | 20 ++++++++ examples/boilerplates/solid-ts/src/index.tsx | 24 +++++----- 8 files changed, 98 insertions(+), 46 deletions(-) create mode 100644 examples/boilerplates/react-buildless/styles.css diff --git a/examples/boilerplates/react-buildless/index.html b/examples/boilerplates/react-buildless/index.html index dc1a3530..31f61c61 100644 --- a/examples/boilerplates/react-buildless/index.html +++ b/examples/boilerplates/react-buildless/index.html @@ -1,7 +1,11 @@ - - -
+ + + + + + + + + +
+ diff --git a/examples/boilerplates/solid-ts/my-window.zebar.json b/examples/boilerplates/solid-ts/my-window.zebar.json index 88ce7806..48bdd0f2 100644 --- a/examples/boilerplates/solid-ts/my-window.zebar.json +++ b/examples/boilerplates/solid-ts/my-window.zebar.json @@ -10,10 +10,10 @@ "placements": [ { "anchor": "top_left", - "offsetX": "20px", - "offsetY": "20px", + "offsetX": "0px", + "offsetY": "0px", "width": "100%", - "height": "30px", + "height": "40px", "monitorSelection": { "type": "all" } diff --git a/examples/boilerplates/solid-ts/src/index.css b/examples/boilerplates/solid-ts/src/index.css index de672762..a81cadc7 100644 --- a/examples/boilerplates/solid-ts/src/index.css +++ b/examples/boilerplates/solid-ts/src/index.css @@ -1,3 +1,23 @@ +body { + color: rgb(255 255 255 / 90%); + font-family: ui-monospace, monospace; + font-size: 12px; +} + +html, +body, +#root { + height: 100%; +} + .app { text-align: center; } + +.chip { + display: inline-block; + padding: 4px 8px; + border-radius: 4px; + background: rgb(84, 66, 220); + margin-right: 4px; +} diff --git a/examples/boilerplates/solid-ts/src/index.tsx b/examples/boilerplates/solid-ts/src/index.tsx index 0169b3a4..fd010218 100644 --- a/examples/boilerplates/solid-ts/src/index.tsx +++ b/examples/boilerplates/solid-ts/src/index.tsx @@ -5,6 +5,7 @@ import { createStore } from 'solid-js/store'; import { init } from 'zebar'; const zebarCtx = await init(); + const [cpu, battery, memory, weather] = await Promise.all([ zebarCtx.createProvider({ type: 'cpu' }), zebarCtx.createProvider({ type: 'battery' }), @@ -15,25 +16,26 @@ const [cpu, battery, memory, weather] = await Promise.all([ render(() => , document.getElementById('root')!); function App() { - const [store, setStore] = createStore({ + const [outputs, setOutputs] = createStore({ cpu: cpu.output, battery: battery.output, memory: memory.output, weather: weather.output, }); - cpu.onOutput(cpu => setStore({ cpu })); - battery.onOutput(battery => setStore({ battery })); - memory.onOutput(memory => setStore({ memory })); - weather.onOutput(weather => setStore({ weather })); + cpu.onOutput(cpu => setOutputs({ cpu })); + battery.onOutput(battery => setOutputs({ battery })); + memory.onOutput(memory => setOutputs({ memory })); + weather.onOutput(weather => setOutputs({ weather })); return ( -
- cpu: {store.cpu.usage} - battery: {store.battery?.chargePercent} - memory: {store.memory.usage} - weather temp: {store.weather.celsiusTemp} - weather status: {store.weather.status} +
+
CPU usage: {outputs.cpu.usage}
+
+ Battery charge: {outputs.battery?.chargePercent} +
+
Memory usage: {outputs.memory.usage}
+
Weather temp: {outputs.weather?.celsiusTemp}
); } From 14e241e58b7a7fbe233650dd8c67b2d2a241be04 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Wed, 11 Sep 2024 19:56:22 +0800 Subject: [PATCH 118/138] fix: remove starter config installation in wix config --- packages/desktop/installer.wxs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/packages/desktop/installer.wxs b/packages/desktop/installer.wxs index 34b5d35f..b6fc9940 100644 --- a/packages/desktop/installer.wxs +++ b/packages/desktop/installer.wxs @@ -71,9 +71,6 @@ - - - @@ -123,19 +120,6 @@ - - - - - ADD_GLAZEWM_STARTER = 1 - - - - - - - - @@ -296,8 +280,6 @@ {{/if}} - - Date: Wed, 11 Sep 2024 20:52:33 +0800 Subject: [PATCH 119/138] feat: add helper message for running solid-ts example --- examples/boilerplates/solid-ts/dist/index.html | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 examples/boilerplates/solid-ts/dist/index.html diff --git a/examples/boilerplates/solid-ts/dist/index.html b/examples/boilerplates/solid-ts/dist/index.html new file mode 100644 index 00000000..5c6c9a50 --- /dev/null +++ b/examples/boilerplates/solid-ts/dist/index.html @@ -0,0 +1,15 @@ + + + + + + Zebar + + +

+ Boilerplate for SolidJS with TypeScript. Run npm i and + npm run dev in the solid-ts directory to + run this example. +

+ + From ff85b2c15889411692cc297b25e93749ea1bc0f9 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Wed, 11 Sep 2024 21:31:29 +0800 Subject: [PATCH 120/138] feat: improve error messages of window config parse errors --- packages/desktop/src/config.rs | 60 +++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/packages/desktop/src/config.rs b/packages/desktop/src/config.rs index 4d458d15..da810f22 100644 --- a/packages/desktop/src/config.rs +++ b/packages/desktop/src/config.rs @@ -32,8 +32,8 @@ pub struct WindowConfig { #[serde(rename = "$schema")] schema: Option, - /// Entry point HTML file. - pub html_path: String, + /// Relative path to entry point HTML file. + pub html_path: PathBuf, /// Default options for when the window is opened. pub launch_options: WindowLaunchOptions, @@ -276,24 +276,46 @@ impl Config { // Recursively aggregate configs in subdirectories. configs.extend(Self::read_window_configs(&path)?); } else if has_extension(&path, ".zebar.json") { - if let Ok(config) = read_and_parse_json::(&path) { - let config_path = path.to_absolute()?; - - let html_path = path - .parent() - .context("Invalid parent directory.")? - .join(&config.html_path) - .to_absolute()?; - - info!("Found valid window config at: {}", config_path.display()); - - configs.push(WindowConfigEntry { - config, - config_path, - html_path, + let parse_res = read_and_parse_json::(&path) + .and_then(|config| { + let config_path = path.to_absolute()?; + + let html_path = path + .parent() + .and_then(|parent| { + parent.join(&config.html_path).to_absolute().ok() + }) + .with_context(|| { + format!( + "HTML file not found at {} for config {}.", + config.html_path.display(), + config_path.display() + ) + })?; + + Ok(WindowConfigEntry { + config, + config_path, + html_path, + }) }); - } else { - error!("Failed to parse config: {}", path.display()); + + match parse_res { + Ok(config) => { + info!( + "Found valid window config at: {}", + config.config_path.display() + ); + + configs.push(config); + } + Err(err) => { + error!( + "Failed to parse config at {}: {:?}", + path.display(), + err + ); + } } } } From 4d72e60d71fe34053461a7a698ae3d595e2d232b Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Wed, 11 Sep 2024 21:54:22 +0800 Subject: [PATCH 121/138] feat: change to windows subsystem --- packages/desktop/src/main.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/desktop/src/main.rs b/packages/desktop/src/main.rs index d440806e..d9702452 100644 --- a/packages/desktop/src/main.rs +++ b/packages/desktop/src/main.rs @@ -1,3 +1,5 @@ +// Prevent additional console window on Windows in release mode. +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![feature(async_closure)] #![feature(iterator_try_collect)] @@ -34,6 +36,15 @@ mod window_factory; /// subcommand. #[tokio::main] async fn main() -> anyhow::Result<()> { + // Attach to parent console on Windows in release mode. + #[cfg(all(windows, not(debug_assertions)))] + { + use windows::Win32::System::Console::{ + AttachConsole, ATTACH_PARENT_PROCESS, + }; + let _ = unsafe { AttachConsole(ATTACH_PARENT_PROCESS) }; + } + let cli = Cli::parse(); match cli.command() { From 0ae11d13595f0bc9aae07ab757c5db431e1ffb63 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Wed, 11 Sep 2024 22:12:23 +0800 Subject: [PATCH 122/138] chore: add missing `Win32_System_Console` feature --- packages/desktop/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 668c3ab9..b8c37ab0 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -39,7 +39,7 @@ regex = "1" [target.'cfg(target_os = "windows")'.dependencies] komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.28" } -windows = { version = "0.57", features = [] } +windows = { version = "0.57", features = ["Win32_System_Console"] } [target.'cfg(target_os = "macos")'.dependencies] cocoa = "0.25" From 16cde9eb81e74f224c370dcab3dfe2bdddf76654 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 12 Sep 2024 03:45:08 +0800 Subject: [PATCH 123/138] fix: prevent terminal from showing on `netsh` command --- .../src/providers/network/wifi_hotspot.rs | 72 ++++++++++--------- 1 file changed, 39 insertions(+), 33 deletions(-) diff --git a/packages/desktop/src/providers/network/wifi_hotspot.rs b/packages/desktop/src/providers/network/wifi_hotspot.rs index de3143f0..ef022ac9 100644 --- a/packages/desktop/src/providers/network/wifi_hotspot.rs +++ b/packages/desktop/src/providers/network/wifi_hotspot.rs @@ -1,7 +1,9 @@ -use std::process::Command; +use std::{os::windows::process::CommandExt, process::Command}; use anyhow::Context; use regex::Regex; +#[cfg(target_os = "windows")] +use windows::Win32::System::Threading::CREATE_NO_WINDOW; #[derive(Debug)] pub struct WifiHotstop { @@ -15,47 +17,51 @@ pub struct WifiHotstop { /// Unclear if the primary interface is always the default interface /// returned by the netdev/defaultnet crate - requires more testing. pub fn default_gateway_wifi() -> anyhow::Result { - if cfg!(not(target_os = "windows")) { - return Ok(WifiHotstop { + #[cfg(not(target_os = "windows"))] + { + Ok(WifiHotstop { ssid: None, signal_strength: None, - }); + }) } + #[cfg(target_os = "windows")] + { + let ssid_match = Regex::new(r"(?m)^\s*SSID\s*:\s*(.*?)\r?$").unwrap(); - let ssid_match = Regex::new(r"(?m)^\s*SSID\s*:\s*(.*?)\r?$").unwrap(); + let signal_match = + Regex::new(r"(?m)^\s*Signal\s*:\s*(.*?)\r?$").unwrap(); - let signal_match = - Regex::new(r"(?m)^\s*Signal\s*:\s*(.*?)\r?$").unwrap(); + let signal_strip = Regex::new(r"(\d+)%").unwrap(); - let signal_strip = Regex::new(r"(\d+)%").unwrap(); + let output = Command::new("netsh") + .args(&["wlan", "show", "interfaces"]) + .creation_flags(CREATE_NO_WINDOW.0) + .output() + .context("Could not run netsh.")?; - let output = Command::new("netsh") - .args(&["wlan", "show", "interfaces"]) - .output() - .context("Could not run netsh.")?; + let output = String::from_utf8_lossy(&output.stdout); - let output = String::from_utf8_lossy(&output.stdout); + let ssid = ssid_match + .captures(&output) + .context("Failed to parse WiFi hotspot SSID.")? + .get(1) + .map(|s| s.as_str().to_string()); - let ssid = ssid_match - .captures(&output) - .context("Failed to parse WiFi hotspot SSID.")? - .get(1) - .map(|s| s.as_str().to_string()); + let signal_str = signal_match + .captures(&output) + .context("Failed to parse WiFi hotspot signal strength.")? + .get(1) + .map(|s| s.as_str()); - let signal_str = signal_match - .captures(&output) - .context("Failed to parse WiFi hotspot signal strength.")? - .get(1) - .map(|s| s.as_str()); + let signal = signal_str + .and_then(|s| signal_strip.captures(s)) + .context("Failed to parse WiFi hotspot signal strength.")? + .get(1) + .and_then(|s| s.as_str().parse().ok()); - let signal = signal_str - .and_then(|s| signal_strip.captures(s)) - .context("Failed to parse WiFi hotspot signal strength.")? - .get(1) - .and_then(|s| s.as_str().parse().ok()); - - return Ok(WifiHotstop { - ssid, - signal_strength: signal, - }); + Ok(WifiHotstop { + ssid, + signal_strength: signal, + }) + } } From 8287e32455414f0f1ec4b061915fd23290029e78 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 12 Sep 2024 03:49:42 +0800 Subject: [PATCH 124/138] feat: queue client connection error from glazewm provider --- .../glazewm/create-glazewm-provider.ts | 252 +++++++++--------- 1 file changed, 130 insertions(+), 122 deletions(-) diff --git a/packages/client-api/src/providers/glazewm/create-glazewm-provider.ts b/packages/client-api/src/providers/glazewm/create-glazewm-provider.ts index 32e17122..8715860d 100644 --- a/packages/client-api/src/providers/glazewm/create-glazewm-provider.ts +++ b/packages/client-api/src/providers/glazewm/create-glazewm-provider.ts @@ -108,142 +108,150 @@ export async function createGlazeWmProvider( const mergedConfig = glazeWmProviderConfigSchema.parse(config); return createBaseProvider(mergedConfig, async queue => { - const monitors = await getMonitors(); - const client = new WmClient(); - - let state = await getInitialState(); - queue.output(state); - - const unlisten = await client.subscribeMany( - [ - WmEventType.BINDING_MODES_CHANGED, - WmEventType.FOCUS_CHANGED, - WmEventType.FOCUSED_CONTAINER_MOVED, - WmEventType.TILING_DIRECTION_CHANGED, - WmEventType.WORKSPACE_ACTIVATED, - WmEventType.WORKSPACE_DEACTIVATED, - WmEventType.WORKSPACE_UPDATED, - ], - onEvent, - ); - - // TODO: Update state when monitors change. - // monitors.onChange(async () => { - // state = { ...state, ...(await getMonitorState()) }; - // queue.value(state); - // }); - - async function onEvent( - e: - | BindingModesChangedEvent - | FocusChangedEvent - | FocusedContainerMovedEvent - | TilingDirectionChangedEvent - | WorkspaceActivatedEvent - | WorkspaceDeactivatedEvent - | WorkspaceUpdatedEvent, - ) { - switch (e.eventType) { - case WmEventType.BINDING_MODES_CHANGED: { - state = { ...state, bindingModes: e.newBindingModes }; - break; - } - case WmEventType.FOCUS_CHANGED: { - state = { ...state, focusedContainer: e.focusedContainer }; - state = { ...state, ...(await getMonitorState()) }; - - const { tilingDirection } = await client.queryTilingDirection(); - state = { ...state, tilingDirection }; - break; - } - case WmEventType.FOCUSED_CONTAINER_MOVED: { - state = { ...state, focusedContainer: e.focusedContainer }; - state = { ...state, ...(await getMonitorState()) }; - break; - } - case WmEventType.TILING_DIRECTION_CHANGED: { - state = { ...state, tilingDirection: e.newTilingDirection }; - break; - } - case WmEventType.WORKSPACE_ACTIVATED: - case WmEventType.WORKSPACE_DEACTIVATED: - case WmEventType.WORKSPACE_UPDATED: { - state = { ...state, ...(await getMonitorState()) }; - break; - } - } + try { + const monitors = await getMonitors(); + const client = new WmClient(); + let state = await getInitialState(); queue.output(state); - } - function runCommand( - command: string, - subjectContainerId?: string, - ): Promise { - return client.runCommand(command, subjectContainerId); - } + const unlisten = await client.subscribeMany( + [ + WmEventType.BINDING_MODES_CHANGED, + WmEventType.FOCUS_CHANGED, + WmEventType.FOCUSED_CONTAINER_MOVED, + WmEventType.TILING_DIRECTION_CHANGED, + WmEventType.WORKSPACE_ACTIVATED, + WmEventType.WORKSPACE_DEACTIVATED, + WmEventType.WORKSPACE_UPDATED, + ], + onEvent, + ); - async function getInitialState() { - const { focused: focusedContainer } = await client.queryFocused(); - const { bindingModes } = await client.queryBindingModes(); - const { tilingDirection } = await client.queryTilingDirection(); - - return { - ...(await getMonitorState()), - focusedContainer, - tilingDirection, - bindingModes, - runCommand, - }; - } + // TODO: Update state when monitors change. + // monitors.onChange(async () => { + // state = { ...state, ...(await getMonitorState()) }; + // queue.value(state); + // }); + + async function onEvent( + e: + | BindingModesChangedEvent + | FocusChangedEvent + | FocusedContainerMovedEvent + | TilingDirectionChangedEvent + | WorkspaceActivatedEvent + | WorkspaceDeactivatedEvent + | WorkspaceUpdatedEvent, + ) { + switch (e.eventType) { + case WmEventType.BINDING_MODES_CHANGED: { + state = { ...state, bindingModes: e.newBindingModes }; + break; + } + case WmEventType.FOCUS_CHANGED: { + state = { ...state, focusedContainer: e.focusedContainer }; + state = { ...state, ...(await getMonitorState()) }; + + const { tilingDirection } = + await client.queryTilingDirection(); + state = { ...state, tilingDirection }; + break; + } + case WmEventType.FOCUSED_CONTAINER_MOVED: { + state = { ...state, focusedContainer: e.focusedContainer }; + state = { ...state, ...(await getMonitorState()) }; + break; + } + case WmEventType.TILING_DIRECTION_CHANGED: { + state = { ...state, tilingDirection: e.newTilingDirection }; + break; + } + case WmEventType.WORKSPACE_ACTIVATED: + case WmEventType.WORKSPACE_DEACTIVATED: + case WmEventType.WORKSPACE_UPDATED: { + state = { ...state, ...(await getMonitorState()) }; + break; + } + } - async function getMonitorState() { - const currentPosition = { - x: monitors.currentMonitor!.x, - y: monitors.currentMonitor!.y, - }; + queue.output(state); + } - const { monitors: glazeWmMonitors } = await client.queryMonitors(); + function runCommand( + command: string, + subjectContainerId?: string, + ): Promise { + return client.runCommand(command, subjectContainerId); + } - // Get GlazeWM monitor that corresponds to the Zebar window's monitor. - const currentGlazeWmMonitor = glazeWmMonitors.reduce((a, b) => - getCoordinateDistance(currentPosition, a) < - getCoordinateDistance(currentPosition, b) - ? a - : b, - ); + async function getInitialState() { + const { focused: focusedContainer } = await client.queryFocused(); + const { bindingModes } = await client.queryBindingModes(); + const { tilingDirection } = await client.queryTilingDirection(); + + return { + ...(await getMonitorState()), + focusedContainer, + tilingDirection, + bindingModes, + runCommand, + }; + } - const focusedGlazeWmMonitor = glazeWmMonitors.find( - monitor => monitor.hasFocus, - ); + async function getMonitorState() { + const currentPosition = { + x: monitors.currentMonitor!.x, + y: monitors.currentMonitor!.y, + }; - const allGlazeWmWorkspaces = glazeWmMonitors.flatMap( - monitor => monitor.children, - ); + const { monitors: glazeWmMonitors } = await client.queryMonitors(); - const focusedGlazeWmWorkspace = focusedGlazeWmMonitor?.children.find( - workspace => workspace.hasFocus, - ); + // Get GlazeWM monitor that corresponds to the Zebar window's monitor. + const currentGlazeWmMonitor = glazeWmMonitors.reduce((a, b) => + getCoordinateDistance(currentPosition, a) < + getCoordinateDistance(currentPosition, b) + ? a + : b, + ); + + const focusedGlazeWmMonitor = glazeWmMonitors.find( + monitor => monitor.hasFocus, + ); - const displayedGlazeWmWorkspace = - currentGlazeWmMonitor.children.find( - workspace => workspace.isDisplayed, + const allGlazeWmWorkspaces = glazeWmMonitors.flatMap( + monitor => monitor.children, ); - return { - displayedWorkspace: displayedGlazeWmWorkspace!, - focusedWorkspace: focusedGlazeWmWorkspace!, - currentWorkspaces: currentGlazeWmMonitor.children, - allWorkspaces: allGlazeWmWorkspaces, - focusedMonitor: focusedGlazeWmMonitor!, - currentMonitor: currentGlazeWmMonitor, - allMonitors: glazeWmMonitors, + const focusedGlazeWmWorkspace = + focusedGlazeWmMonitor?.children.find( + workspace => workspace.hasFocus, + ); + + const displayedGlazeWmWorkspace = + currentGlazeWmMonitor.children.find( + workspace => workspace.isDisplayed, + ); + + return { + displayedWorkspace: displayedGlazeWmWorkspace!, + focusedWorkspace: focusedGlazeWmWorkspace!, + currentWorkspaces: currentGlazeWmMonitor.children, + allWorkspaces: allGlazeWmWorkspaces, + focusedMonitor: focusedGlazeWmMonitor!, + currentMonitor: currentGlazeWmMonitor, + allMonitors: glazeWmMonitors, + }; + } + + return () => { + unlisten(); + client.closeConnection(); }; + } catch (err) { + // TODO: Implement retries. + queue.error((err as Error).message); + return () => {}; } - - return () => { - unlisten(); - client.closeConnection(); - }; }); } From 4b54b53966b52065a30718db4463049e2ea5db97 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 12 Sep 2024 03:52:05 +0800 Subject: [PATCH 125/138] refactor: remove config reload test --- packages/desktop/src/main.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/desktop/src/main.rs b/packages/desktop/src/main.rs index d9702452..a71f16e5 100644 --- a/packages/desktop/src/main.rs +++ b/packages/desktop/src/main.rs @@ -149,13 +149,6 @@ fn start_app(cli: Cli) -> anyhow::Result<()> { SysTray::new(app.handle(), config.clone(), window_factory) .await?; - // TODO: Remove this test. - tokio::spawn(async move { - sleep(Duration::from_secs(60)).await; - println!("Reloading config..."); - config.reload().await.unwrap(); - }); - Ok(()) }) }) From 470c058c2b934c182912610129bdfc299e2bc491 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 12 Sep 2024 05:56:25 +0800 Subject: [PATCH 126/138] chore: bump `glazewm` dep to latest --- packages/client-api/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/client-api/package.json b/packages/client-api/package.json index b5aad18c..7efbe07f 100644 --- a/packages/client-api/package.json +++ b/packages/client-api/package.json @@ -28,7 +28,7 @@ "@tauri-apps/api": "2.0.0-beta.15", "@tauri-apps/plugin-dialog": "2.0.0-beta.7", "@tauri-apps/plugin-shell": "2.0.0-beta.8", - "glazewm": "1.4.1", + "glazewm": "1.4.3", "luxon": "3.4.4", "zod": "3.22.4" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d3aa90d8..6922fc5e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,8 +46,8 @@ importers: specifier: 2.0.0-beta.8 version: 2.0.0-beta.8 glazewm: - specifier: 1.4.1 - version: 1.4.1 + specifier: 1.4.3 + version: 1.4.3 luxon: specifier: 3.4.4 version: 3.4.4 @@ -956,8 +956,8 @@ packages: git-hooks-list@3.1.0: resolution: {integrity: sha512-LF8VeHeR7v+wAbXqfgRlTSX/1BJR9Q1vEMR8JAz1cEg6GX07+zyj3sAdDvYjj/xnlIfVuGgj4qBei1K3hKH+PA==} - glazewm@1.4.1: - resolution: {integrity: sha512-rltNAKzTY/g8XEGR+LkyDFIhw9AOq0GW0FqT1RAe54KN738YuPR+yJ72UIDfrXwSpH1Ztb6Ii72qBQPaOjlvNg==} + glazewm@1.4.3: + resolution: {integrity: sha512-V5YFRjI9SNu6TODloDnZuEH4lstyb+d1N01H8OLgVba2uzfCwt0ateJ7U96Dh0WQfHxJPaS4q1XeesHeOVFrIg==} engines: {node: '>=12'} peerDependencies: ws: '*' @@ -2324,7 +2324,7 @@ snapshots: git-hooks-list@3.1.0: {} - glazewm@1.4.1: + glazewm@1.4.3: dependencies: tslib: 2.6.1 From ff485870cc158a48309380eb1066823decf3ffac Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 12 Sep 2024 15:23:21 +0800 Subject: [PATCH 127/138] fix: change `wifi_hotspot` to compile on all platforms --- packages/desktop/src/providers/network/wifi_hotspot.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/desktop/src/providers/network/wifi_hotspot.rs b/packages/desktop/src/providers/network/wifi_hotspot.rs index ef022ac9..05307557 100644 --- a/packages/desktop/src/providers/network/wifi_hotspot.rs +++ b/packages/desktop/src/providers/network/wifi_hotspot.rs @@ -1,4 +1,6 @@ -use std::{os::windows::process::CommandExt, process::Command}; +#[cfg(target_os = "windows")] +use std::os::windows::process::CommandExt; +use std::process::Command; use anyhow::Context; use regex::Regex; From 45be43156e848f6a0bd73c1750dff46a20420b43 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 12 Sep 2024 21:23:32 +0800 Subject: [PATCH 128/138] feat: add `createProviderGroup` method on ctx --- examples/starter/vanilla.html | 94 ++++-------- packages/client-api/src/init.ts | 5 +- .../src/providers/create-base-provider.ts | 30 ++-- .../src/providers/create-provider-group.ts | 145 ++++++++++++++++++ .../src/providers/create-provider.ts | 105 ++++++++----- packages/client-api/src/providers/index.ts | 10 +- 6 files changed, 260 insertions(+), 129 deletions(-) create mode 100644 packages/client-api/src/providers/create-provider-group.ts diff --git a/examples/starter/vanilla.html b/examples/starter/vanilla.html index 825c3bad..f3b21c80 100644 --- a/examples/starter/vanilla.html +++ b/examples/starter/vanilla.html @@ -26,72 +26,44 @@ import { createRoot } from 'react-dom/client'; import { init } from 'zebar'; - const zebarCtx = await init(); - - const [network, cpu, date, battery, memory, weather] = - await Promise.all([ - zebarCtx.createProvider({ type: 'network' }), - zebarCtx.createProvider({ type: 'cpu' }), - zebarCtx.createProvider({ - type: 'date', - formatting: 'EEE d MMM t', - }), - zebarCtx.createProvider({ type: 'battery' }), - zebarCtx.createProvider({ type: 'memory' }), - zebarCtx.createProvider({ type: 'weather' }), - ]); + const ctx = await init(); + + const providers = await ctx.createProviderGroup({ + network: { type: 'network' }, + cpu: { type: 'cpu' }, + date: { type: 'date', formatting: 'EEE d MMM t' }, + battery: { type: 'battery' }, + memory: { type: 'memory' }, + weather: { type: 'weather' }, + }); createRoot(document.getElementById('root')).render(); function App() { - const [outputs, setOutputs] = useState({ - network: network.output, - cpu: cpu.output, - date: date.output, - battery: battery.output, - memory: memory.output, - weather: weather.output, - }); + const [output, setOutput] = useState(providers.outputs); useEffect(() => { - network.onOutput(network => - setOutputs(outputs => ({ ...outputs, network })), - ); - cpu.onOutput(cpu => - setOutputs(outputs => ({ ...outputs, cpu })), - ); - date.onOutput(date => - setOutputs(outputs => ({ ...outputs, date })), - ); - battery.onOutput(battery => - setOutputs(outputs => ({ ...outputs, battery })), - ); - memory.onOutput(memory => - setOutputs(outputs => ({ ...outputs, memory })), - ); - weather.onOutput(weather => - setOutputs(outputs => ({ ...outputs, weather })), - ); + providers.onOutput(() => setOutput(providers.outputs)); }, []); // Get icon to show for current network status. function getNetworkIcon() { - switch (outputs.network.defaultInterface?.type) { + switch (output.network.defaultInterface?.type) { case 'ethernet': return ; case 'wifi': - if (outputs.network.defaultGateway?.signalStrength >= 80) { + if (output.network.defaultGateway?.signalStrength >= 80) { return ; } else if ( - outputs.network.defaultGateway?.signalStrength >= 65 + output.network.defaultGateway?.signalStrength >= 65 ) { return ; } else if ( - outputs.network.defaultGateway?.signalStrength >= 40 + output.network.defaultGateway?.signalStrength >= 40 ) { return ; } else if ( - outputs.network.defaultGateway?.signalStrength >= 25 + output.network.defaultGateway?.signalStrength >= 25 ) { return ; } else { @@ -106,20 +78,20 @@ // Get icon to show for how much of the battery is charged. function getBatteryIcon() { - if (outputs.battery.chargePercent > 90) + if (output.battery.chargePercent > 90) return ; - if (outputs.battery.chargePercent > 70) + if (output.battery.chargePercent > 70) return ; - if (outputs.battery.chargePercent > 40) + if (output.battery.chargePercent > 40) return ; - if (outputs.battery.chargePercent > 20) + if (output.battery.chargePercent > 20) return ; return ; } // Get icon to show for current weather status. function getWeatherIcon() { - switch (outputs.weather.status) { + switch (output.weather.status) { case 'clear_day': return ; case 'clear_night': @@ -153,19 +125,19 @@
-
{outputs.date.formatted}
+
{output.date.formatted}
- {outputs.network && ( + {output.network && (
{getNetworkIcon()} - {outputs.network.defaultGateway?.ssid} + {output.network.defaultGateway?.ssid}
)}
- {Math.round(outputs.memory.usage)}% + {Math.round(output.memory.usage)}%
@@ -173,27 +145,27 @@ {/* Change the text color if the CPU usage is high. */} 85 ? 'high-usage' : ''} + className={output.cpu.usage > 85 ? 'high-usage' : ''} > - {Math.round(outputs.cpu.usage)}% + {Math.round(output.cpu.usage)}%
- {outputs.battery && ( + {output.battery && (
{/* Show icon for whether battery is charging. */} - {outputs.battery.isCharging && ( + {output.battery.isCharging && ( )} {getBatteryIcon()} - {Math.round(outputs.battery.chargePercent)}% + {Math.round(output.battery.chargePercent)}%
)} - {outputs.weather && ( + {output.weather && (
{getWeatherIcon()} - {Math.round(outputs.weather.celsiusTemp)}°C + {Math.round(output.weather.celsiusTemp)}°C
)}
diff --git a/packages/client-api/src/init.ts b/packages/client-api/src/init.ts index 726f2240..6a098c0f 100644 --- a/packages/client-api/src/init.ts +++ b/packages/client-api/src/init.ts @@ -6,10 +6,10 @@ import { openWindow, setWindowZOrder, showErrorDialog, -} from './desktop'; +} from '~/desktop'; import { createLogger } from '~/utils'; +import { createProvider, createProviderGroup } from '~/providers'; import type { ZebarContext } from './zebar-context.model'; -import { createProvider } from './providers'; const logger = createLogger('init-window'); @@ -36,6 +36,7 @@ export async function init(): Promise { return openWindow(absolutePath); }, createProvider, + createProviderGroup, currentWindow: { ...windowState, tauri: currentWindow, diff --git a/packages/client-api/src/providers/create-base-provider.ts b/packages/client-api/src/providers/create-base-provider.ts index bdebb665..1fcead1f 100644 --- a/packages/client-api/src/providers/create-base-provider.ts +++ b/packages/client-api/src/providers/create-base-provider.ts @@ -1,13 +1,13 @@ import { Deferred } from '~/utils'; import type { ProviderConfig } from './create-provider'; -export interface Provider { +export interface Provider { /** * Latest output emitted from the provider. * * `null` if the latest emission from the provider is an error. */ - output: TVal | null; + output: TOutput | null; /** * Latest error message emitted from the provider. @@ -41,7 +41,7 @@ export interface Provider { * * @param callback - Callback to run when an output is emitted. */ - onOutput(callback: (output: TVal) => void): void; + onOutput(callback: (output: TOutput) => void): void; /** * Listens for errors from the provider. @@ -63,16 +63,16 @@ type ProviderFetcher = (queue: { export async function createBaseProvider< TConfig extends ProviderConfig, - TVal, + TOutput, >( config: TConfig, - fetcher: ProviderFetcher, -): Promise> { - const valueListeners = new Set<(val: TVal) => void>(); + fetcher: ProviderFetcher, +): Promise> { + const outputListeners = new Set<(output: TOutput) => void>(); const errorListeners = new Set<(error: string) => void>(); let latestEmission = { - value: null as TVal | null, + output: null as TOutput | null, error: null as string | null, hasError: false, }; @@ -83,13 +83,13 @@ export async function createBaseProvider< const hasFirstEmit = new Deferred(); const unlisten = await fetcher({ - output: value => { - latestEmission = { value, error: null, hasError: false }; - valueListeners.forEach(listener => listener(value)); + output: output => { + latestEmission = { output, error: null, hasError: false }; + outputListeners.forEach(listener => listener(output)); hasFirstEmit.resolve(); }, error: error => { - latestEmission = { value: null, error, hasError: true }; + latestEmission = { output: null, error, hasError: true }; errorListeners.forEach(listener => listener(error)); hasFirstEmit.resolve(); }, @@ -103,7 +103,7 @@ export async function createBaseProvider< return { get output() { - return latestEmission.value; + return latestEmission.output; }, get error() { return latestEmission.error; @@ -120,7 +120,7 @@ export async function createBaseProvider< await startFetcher(); }, stop: async () => { - valueListeners.clear(); + outputListeners.clear(); errorListeners.clear(); if (unlisten) { @@ -128,7 +128,7 @@ export async function createBaseProvider< } }, onOutput: callback => { - valueListeners.add(callback); + outputListeners.add(callback); }, onError: callback => { errorListeners.add(callback); diff --git a/packages/client-api/src/providers/create-provider-group.ts b/packages/client-api/src/providers/create-provider-group.ts new file mode 100644 index 00000000..8ab688f2 --- /dev/null +++ b/packages/client-api/src/providers/create-provider-group.ts @@ -0,0 +1,145 @@ +import { + createProvider, + type ProviderConfig, + type ProviderMap, +} from './create-provider'; + +export type ProviderGroupConfig = { + [name: string]: ProviderConfig; +}; + +export type ProviderGroupOutputs = { + [N in keyof T]: ProviderMap[T[N]['type']]['output']; +}; + +export type ProviderGroupErrors = { + [N in keyof T]: ProviderMap[T[N]['type']]['error']; +}; + +export type ProviderGroupRaw = { + [N in keyof T]: ProviderMap[T[N]['type']]; +}; + +export type ProviderGroup = { + outputs: ProviderGroupOutputs; + + errors: ProviderGroupErrors; + + /** + * Whether the group has any errors. + */ + hasErrors: boolean; + + /** + * Underlying providers for the group. + */ + raw: ProviderGroupRaw; + + /** + * Config for the provider group. + */ + config: T; + + /** + * TODO + * @param callback TODO + */ + onOutput: (callback: (outputs: ProviderGroupOutputs) => void) => void; + + /** + * TODO + * @param callback TODO + */ + onError: (callback: (errors: ProviderGroupErrors) => void) => void; + + /** + * Restarts all providers in the group. + */ + restartAll(): Promise; + + /** + * Stops all providers in the group. + */ + stopAll(): Promise; +}; + +const xxx = await createProviderGroup({ + glazewm: { type: 'glazewm' }, +}); + +const config = xxx.config; +const output = xxx.outputs.glazewm; +const raw = xxx.raw.glazewm; + +export async function createProviderGroup( + config: T, +): Promise> { + const outputListeners = new Set< + (outputs: ProviderGroupOutputs) => void + >(); + + const errorListeners = new Set< + (errors: ProviderGroupErrors) => void + >(); + + const providerEntries = await Promise.all([ + ...Object.entries(config).map(async ([key, value]) => { + return [key, await createProvider(value)] as const; + }), + ]); + + const providers = Object.fromEntries( + providerEntries, + ) as ProviderGroupRaw; + + let outputs = {} as ProviderGroupOutputs; + let errors = {} as ProviderGroupErrors; + + for (const [name, provider] of providerEntries) { + outputs = { ...outputs, [name]: provider.output }; + errors = { ...errors, [name]: provider.error }; + + provider.onOutput(() => { + outputs = { ...outputs, [name]: provider.output }; + outputListeners.forEach(listener => listener(outputs)); + }); + + provider.onError(() => { + errors = { ...errors, [name]: provider.error }; + errorListeners.forEach(listener => listener(errors)); + }); + } + + return { + get outputs() { + return outputs; + }, + get errors() { + return errors; + }, + get hasErrors() { + return Object.keys(errors).length > 0; + }, + config, + raw: providers, + onOutput: callback => { + outputListeners.add(callback); + }, + onError: callback => { + errorListeners.add(callback); + }, + restartAll: async () => { + await Promise.all( + providerEntries.map(([_, provider]) => provider.restart()), + ); + }, + stopAll: async () => { + outputListeners.clear(); + errorListeners.clear(); + + await Promise.all( + providerEntries.map(([_, provider]) => provider.stop()), + ); + }, + }; +} diff --git a/packages/client-api/src/providers/create-provider.ts b/packages/client-api/src/providers/create-provider.ts index d545b434..43fab2d1 100644 --- a/packages/client-api/src/providers/create-provider.ts +++ b/packages/client-api/src/providers/create-provider.ts @@ -1,102 +1,123 @@ import { createBatteryProvider, + type BatteryProvider, type BatteryProviderConfig, } from './battery/create-battery-provider'; import { createCpuProvider, + type CpuProvider, type CpuProviderConfig, } from './cpu/create-cpu-provider'; import { createDateProvider, + type DateProvider, type DateProviderConfig, } from './date/create-date-provider'; import { createGlazeWmProvider, + type GlazeWmProvider, type GlazeWmProviderConfig, } from './glazewm/create-glazewm-provider'; import { createHostProvider, + type HostProvider, type HostProviderConfig, } from './host/create-host-provider'; import { createIpProvider, + type IpProvider, type IpProviderConfig, } from './ip/create-ip-provider'; import { createKomorebiProvider, + type KomorebiProvider, type KomorebiProviderConfig, } from './komorebi/create-komorebi-provider'; import { createMemoryProvider, + type MemoryProvider, type MemoryProviderConfig, } from './memory/create-memory-provider'; import { createNetworkProvider, + type NetworkProvider, type NetworkProviderConfig, } from './network/create-network-provider'; import { createWeatherProvider, + type WeatherProvider, type WeatherProviderConfig, } from './weather/create-weather-provider'; -export type ProviderConfig = - | BatteryProviderConfig - | CpuProviderConfig - | DateProviderConfig - | GlazeWmProviderConfig - | HostProviderConfig - | IpProviderConfig - | KomorebiProviderConfig - | MemoryProviderConfig - | NetworkProviderConfig - | WeatherProviderConfig; +export interface ProviderConfigMap { + battery: BatteryProviderConfig; + cpu: CpuProviderConfig; + date: DateProviderConfig; + glazewm: GlazeWmProviderConfig; + host: HostProviderConfig; + ip: IpProviderConfig; + komorebi: KomorebiProviderConfig; + memory: MemoryProviderConfig; + network: NetworkProviderConfig; + weather: WeatherProviderConfig; +} -export type ProviderType = ProviderConfig['type']; +export interface ProviderMap { + battery: BatteryProvider; + cpu: CpuProvider; + date: DateProvider; + glazewm: GlazeWmProvider; + host: HostProvider; + ip: IpProvider; + komorebi: KomorebiProvider; + memory: MemoryProvider; + network: NetworkProvider; + weather: WeatherProvider; +} -const createProviderMap = { - battery: createBatteryProvider, - cpu: createCpuProvider, - date: createDateProvider, - glazewm: createGlazeWmProvider, - host: createHostProvider, - ip: createIpProvider, - komorebi: createKomorebiProvider, - memory: createMemoryProvider, - network: createNetworkProvider, - weather: createWeatherProvider, -} as const; +export type ProviderType = keyof ProviderConfigMap; -type ProviderMap = typeof createProviderMap; +export type ProviderConfig = ProviderConfigMap[keyof ProviderConfigMap]; -/** - * Utility type to get the return value of a provider. - * - * @example `Provider<'battery'> = BatteryProvider` - */ -export type ProviderOutput = ReturnType< - ProviderMap[T] ->; +export type ProviderOutput = ProviderMap[keyof ProviderMap]['output']; /** * Creates an instance of a provider. * - * The provider will continue to output until its `destroy` function is + * The provider will continue to output until its `stop` function is * called. * * Waits until the provider has emitted either its first output or first * error. * * @throws If the provider config is invalid. *Does not throw* if the - * provider initially emits an error. + * provider's first emission is an error. */ export function createProvider( config: T, -): ProviderOutput { - const providerFn = createProviderMap[config.type]; - - if (!providerFn) { - throw new Error('Not a supported provider type.'); +): Promise { + switch (config.type) { + case 'battery': + return createBatteryProvider(config) as any; + case 'cpu': + return createCpuProvider(config) as any; + case 'date': + return createDateProvider(config) as any; + case 'glazewm': + return createGlazeWmProvider(config) as any; + case 'host': + return createHostProvider(config) as any; + case 'ip': + return createIpProvider(config) as any; + case 'komorebi': + return createKomorebiProvider(config) as any; + case 'memory': + return createMemoryProvider(config) as any; + case 'network': + return createNetworkProvider(config) as any; + case 'weather': + return createWeatherProvider(config) as any; + default: + throw new Error('Not a supported provider type.'); } - - return providerFn(config as any) as ProviderOutput; } diff --git a/packages/client-api/src/providers/index.ts b/packages/client-api/src/providers/index.ts index c84030a4..008fb252 100644 --- a/packages/client-api/src/providers/index.ts +++ b/packages/client-api/src/providers/index.ts @@ -1,10 +1,2 @@ -// export * from './battery/create-battery-provider'; -// export * from './cpu/create-cpu-provider'; -// export * from './date/create-date-provider'; -// export * from './glazewm/create-glazewm-provider'; -// export * from './ip/create-ip-provider'; -// export * from './memory/create-memory-provider'; -// export * from './network/create-network-provider'; -// export * from './util/create-util-provider'; -// export * from './weather/create-weather-provider'; export * from './create-provider'; +export * from './create-provider-group'; From a5ba4532a5e8ca91e72925d7a2427efca8c78400 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Fri, 13 Sep 2024 20:01:43 +0800 Subject: [PATCH 129/138] feat: improve doc comments on `ZebarContext` --- packages/client-api/src/init.ts | 7 +- .../src/providers/create-base-provider.ts | 2 +- .../src/providers/create-provider-group.ts | 139 ++++++++++-------- .../src/providers/create-provider.ts | 12 +- .../client-api/src/zebar-context.model.ts | 76 ++++++++-- 5 files changed, 146 insertions(+), 90 deletions(-) diff --git a/packages/client-api/src/init.ts b/packages/client-api/src/init.ts index 6a098c0f..23336ff5 100644 --- a/packages/client-api/src/init.ts +++ b/packages/client-api/src/init.ts @@ -27,10 +27,15 @@ export async function init(): Promise { // @ts-ignore - TODO return { openWindow: async (configPath: string) => { + // Ensure the config path ends with '.zebar.json'. + const filePath = configPath.endsWith('.zebar.json') + ? configPath + : `${configPath}.zebar.json`; + const absolutePath = await join( windowState.configPath, '../', - configPath, + filePath, ); return openWindow(absolutePath); diff --git a/packages/client-api/src/providers/create-base-provider.ts b/packages/client-api/src/providers/create-base-provider.ts index 1fcead1f..59a2a3af 100644 --- a/packages/client-api/src/providers/create-base-provider.ts +++ b/packages/client-api/src/providers/create-base-provider.ts @@ -37,7 +37,7 @@ export interface Provider { stop(): Promise; /** - * Listens for changes to the provider's value. + * Listens for outputs from the provider. * * @param callback - Callback to run when an output is emitted. */ diff --git a/packages/client-api/src/providers/create-provider-group.ts b/packages/client-api/src/providers/create-provider-group.ts index 8ab688f2..ba9bfe98 100644 --- a/packages/client-api/src/providers/create-provider-group.ts +++ b/packages/client-api/src/providers/create-provider-group.ts @@ -1,56 +1,72 @@ +import type { ZebarContext } from '~/zebar-context.model'; import { createProvider, type ProviderConfig, type ProviderMap, } from './create-provider'; +/** + * Config for creating multiple provider instances at once. + * + * Keys are unique identifiers for the provider instance, values are their + * respective configs. + */ export type ProviderGroupConfig = { [name: string]: ProviderConfig; }; -export type ProviderGroupOutputs = { - [N in keyof T]: ProviderMap[T[N]['type']]['output']; -}; - -export type ProviderGroupErrors = { - [N in keyof T]: ProviderMap[T[N]['type']]['error']; -}; - -export type ProviderGroupRaw = { - [N in keyof T]: ProviderMap[T[N]['type']]; -}; - export type ProviderGroup = { - outputs: ProviderGroupOutputs; + /** + * A map of combined provider outputs. Each key corresponds to a provider + * name, and each value is the output of that provider. + */ + outputMap: { + [TName in keyof T]: ProviderMap[T[TName]['type']]['output']; + }; - errors: ProviderGroupErrors; + /** + * A map of combined provider errors. Each key corresponds to a provider + * name, and each value is the error of that provider. + */ + errorMap: { + [TName in keyof T]: ProviderMap[T[TName]['type']]['error']; + }; /** - * Whether the group has any errors. + * Whether the latest emission from any provider in the group is an + * error. */ hasErrors: boolean; /** - * Underlying providers for the group. + * Underlying providers in the group. */ - raw: ProviderGroupRaw; + raw: { + [TName in keyof T]: ProviderMap[T[TName]['type']]; + }; /** * Config for the provider group. */ - config: T; + configMap: T; /** - * TODO - * @param callback TODO + * Listens for outputs from any provider in the group. + * + * @param callback - Callback to run when an output is emitted. */ - onOutput: (callback: (outputs: ProviderGroupOutputs) => void) => void; + onOutput: ( + callback: (outputMap: ProviderGroup['outputMap']) => void, + ) => void; /** - * TODO - * @param callback TODO + * Listens for errors from any provider in the group. + * + * @param callback - Callback to run when an error is emitted. */ - onError: (callback: (errors: ProviderGroupErrors) => void) => void; + onError: ( + callback: (errorMap: ProviderGroup['errorMap']) => void, + ) => void; /** * Restarts all providers in the group. @@ -63,65 +79,52 @@ export type ProviderGroup = { stopAll(): Promise; }; -const xxx = await createProviderGroup({ - glazewm: { type: 'glazewm' }, -}); - -const config = xxx.config; -const output = xxx.outputs.glazewm; -const raw = xxx.raw.glazewm; - +/** + * Docs {@link ZebarContext.createProviderGroup} + */ export async function createProviderGroup( - config: T, + configMap: T, ): Promise> { const outputListeners = new Set< - (outputs: ProviderGroupOutputs) => void + (outputMap: ProviderGroup['outputMap']) => void >(); const errorListeners = new Set< - (errors: ProviderGroupErrors) => void + (errorMap: ProviderGroup['errorMap']) => void >(); - const providerEntries = await Promise.all([ - ...Object.entries(config).map(async ([key, value]) => { - return [key, await createProvider(value)] as const; - }), - ]); - - const providers = Object.fromEntries( - providerEntries, - ) as ProviderGroupRaw; + const providerMap = await createProviderMap(configMap); - let outputs = {} as ProviderGroupOutputs; - let errors = {} as ProviderGroupErrors; + let outputMap = {} as ProviderGroup['outputMap']; + let errorMap = {} as ProviderGroup['errorMap']; - for (const [name, provider] of providerEntries) { - outputs = { ...outputs, [name]: provider.output }; - errors = { ...errors, [name]: provider.error }; + for (const [name, provider] of Object.entries(providerMap)) { + outputMap = { ...outputMap, [name]: provider.output }; + errorMap = { ...errorMap, [name]: provider.error }; provider.onOutput(() => { - outputs = { ...outputs, [name]: provider.output }; - outputListeners.forEach(listener => listener(outputs)); + outputMap = { ...outputMap, [name]: provider.output }; + outputListeners.forEach(listener => listener(outputMap)); }); provider.onError(() => { - errors = { ...errors, [name]: provider.error }; - errorListeners.forEach(listener => listener(errors)); + errorMap = { ...errorMap, [name]: provider.error }; + errorListeners.forEach(listener => listener(errorMap)); }); } return { - get outputs() { - return outputs; + get outputMap() { + return outputMap; }, - get errors() { - return errors; + get errorMap() { + return errorMap; }, get hasErrors() { - return Object.keys(errors).length > 0; + return Object.keys(errorMap).length > 0; }, - config, - raw: providers, + configMap, + raw: providerMap, onOutput: callback => { outputListeners.add(callback); }, @@ -130,7 +133,7 @@ export async function createProviderGroup( }, restartAll: async () => { await Promise.all( - providerEntries.map(([_, provider]) => provider.restart()), + Object.values(providerMap).map(provider => provider.restart()), ); }, stopAll: async () => { @@ -138,8 +141,20 @@ export async function createProviderGroup( errorListeners.clear(); await Promise.all( - providerEntries.map(([_, provider]) => provider.stop()), + Object.values(providerMap).map(provider => provider.stop()), ); }, }; } + +async function createProviderMap( + configMap: T, +) { + const providerEntries = await Promise.all([ + ...Object.entries(configMap).map(async ([key, value]) => { + return [key, await createProvider(value)] as const; + }), + ]); + + return Object.fromEntries(providerEntries) as ProviderGroup['raw']; +} diff --git a/packages/client-api/src/providers/create-provider.ts b/packages/client-api/src/providers/create-provider.ts index 43fab2d1..3b66bda3 100644 --- a/packages/client-api/src/providers/create-provider.ts +++ b/packages/client-api/src/providers/create-provider.ts @@ -1,3 +1,4 @@ +import type { ZebarContext } from '~/zebar-context.model'; import { createBatteryProvider, type BatteryProvider, @@ -82,16 +83,7 @@ export type ProviderConfig = ProviderConfigMap[keyof ProviderConfigMap]; export type ProviderOutput = ProviderMap[keyof ProviderMap]['output']; /** - * Creates an instance of a provider. - * - * The provider will continue to output until its `stop` function is - * called. - * - * Waits until the provider has emitted either its first output or first - * error. - * - * @throws If the provider config is invalid. *Does not throw* if the - * provider's first emission is an error. + * Docs {@link ZebarContext.createProvider} */ export function createProvider( config: T, diff --git a/packages/client-api/src/zebar-context.model.ts b/packages/client-api/src/zebar-context.model.ts index 6951de12..d88ee37f 100644 --- a/packages/client-api/src/zebar-context.model.ts +++ b/packages/client-api/src/zebar-context.model.ts @@ -1,14 +1,14 @@ import { Window as TauriWindow } from '@tauri-apps/api/window'; import type { WindowConfig, WindowZOrder } from '~/user-config'; -import type { ProviderConfig, ProviderOutput } from './providers'; +import type { + ProviderConfig, + ProviderGroup, + ProviderGroupConfig, + ProviderMap, +} from './providers'; export interface ZebarContext { - /** - * Parsed window config. - */ - config: WindowConfig; - currentWindow: ZebarWindow; allWindows: ZebarWindow; @@ -26,40 +26,84 @@ export interface ZebarContext { ): Promise; /** - * Initializes a provider. + * Creates an instance of a provider. Alternatively, multiple + * providers can be created using {@link createProviderGroup}. * - * If an existing provider with the same config exists, that provider - * instance will be re-used. + * Waits until the provider has emitted either its first output or first + * error. The provider will continue to output until its `stop` function is + * called. + * + * @throws If the provider config is invalid. *Does not throw* if the + * provider's first emission is an error. */ createProvider( providerConfig: T, - ): ProviderOutput; + ): Promise; + + /** + * Creates multiple provider instances at once. Alternatively, a single + * provider can be created using {@link createProvider}. + */ + createProviderGroup( + configMap: T, + ): Promise>; } export interface ZebarWindow { + /** + * Unique identifier for the window. + */ windowId: string; + + /** + * Parsed window config. + */ config: WindowConfig; + + /** + * Absolute path to the window's config file. + */ configPath: string; + + /** + * Tauri window instance. + */ tauri: TauriWindow; + + /** + * Sets the z-order of the window. + */ setZOrder(zOrder: WindowZOrder): Promise; } export interface Monitor { - /** Human-readable name of the monitor */ + /** + * Human-readable name of the monitor. + */ name: string | null; - /** Width of monitor in physical pixels. */ + /** + * Width of monitor in physical pixels. + */ width: number; - /** Height of monitor in physical pixels. */ + /** + * Height of monitor in physical pixels. + */ height: number; - /** X-coordinate of monitor in physical pixels. */ + /** + * X-coordinate of monitor in physical pixels. + */ x: number; - /** Y-coordinate of monitor in physical pixels. */ + /** + * Y-coordinate of monitor in physical pixels. + */ y: number; - /** Scale factor to map physical pixels to logical pixels. */ + /** + * Scale factor to map physical pixels to logical pixels. + */ scaleFactor: number; } From fdd983fccfb50c6efcad97f71117067d4e1f730e Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Fri, 13 Sep 2024 20:41:27 +0800 Subject: [PATCH 130/138] feat: use `createProviderGroup` in examples --- .../boilerplates/react-buildless/index.html | 42 ++----- examples/boilerplates/solid-ts/src/index.tsx | 32 ++---- examples/starter/vanilla.html | 8 +- examples/starter/with-glazewm.html | 107 +++++++----------- examples/starter/with-komorebi.html | 102 ++++++----------- 5 files changed, 102 insertions(+), 189 deletions(-) diff --git a/examples/boilerplates/react-buildless/index.html b/examples/boilerplates/react-buildless/index.html index 31f61c61..73ea16e8 100644 --- a/examples/boilerplates/react-buildless/index.html +++ b/examples/boilerplates/react-buildless/index.html @@ -28,49 +28,31 @@ const zebarCtx = await init(); - const [cpu, battery, memory, weather] = await Promise.all([ - zebarCtx.createProvider({ type: 'cpu' }), - zebarCtx.createProvider({ type: 'battery' }), - zebarCtx.createProvider({ type: 'memory' }), - zebarCtx.createProvider({ type: 'weather' }), - ]); + const providers = await zebarCtx.createProviderGroup({ + cpu: { type: 'cpu' }, + battery: { type: 'battery' }, + memory: { type: 'memory' }, + weather: { type: 'weather' }, + }); createRoot(document.getElementById('root')).render(); function App() { - const [outputs, setOutputs] = useState({ - cpu: cpu.output, - battery: battery.output, - memory: memory.output, - weather: weather.output, - }); + const [output, setOutput] = useState(providers.outputMap); useEffect(() => { - cpu.onOutput(cpu => - setOutputs(outputs => ({ ...outputs, cpu })), - ); - battery.onOutput(battery => - setOutputs(outputs => ({ ...outputs, battery })), - ); - memory.onOutput(memory => - setOutputs(outputs => ({ ...outputs, memory })), - ); - weather.onOutput(weather => - setOutputs(outputs => ({ ...outputs, weather })), - ); + providers.onOutput(() => setOutput(providers.outputMap)); }, []); return (
-
CPU usage: {outputs.cpu.usage}
+
CPU usage: {output.cpu.usage}
- Battery charge: {outputs.battery?.chargePercent} + Battery charge: {output.battery?.chargePercent}
+
Memory usage: {output.memory.usage}
- Memory usage: {outputs.memory.usage} -
-
- Weather temp: {outputs.weather?.celsiusTemp} + Weather temp: {output.weather?.celsiusTemp}
); diff --git a/examples/boilerplates/solid-ts/src/index.tsx b/examples/boilerplates/solid-ts/src/index.tsx index fd010218..6da68176 100644 --- a/examples/boilerplates/solid-ts/src/index.tsx +++ b/examples/boilerplates/solid-ts/src/index.tsx @@ -6,36 +6,28 @@ import { init } from 'zebar'; const zebarCtx = await init(); -const [cpu, battery, memory, weather] = await Promise.all([ - zebarCtx.createProvider({ type: 'cpu' }), - zebarCtx.createProvider({ type: 'battery' }), - zebarCtx.createProvider({ type: 'memory' }), - zebarCtx.createProvider({ type: 'weather' }), -]); +const providers = await zebarCtx.createProviderGroup({ + cpu: { type: 'cpu' }, + battery: { type: 'battery' }, + memory: { type: 'memory' }, + weather: { type: 'weather' }, +}); render(() => , document.getElementById('root')!); function App() { - const [outputs, setOutputs] = createStore({ - cpu: cpu.output, - battery: battery.output, - memory: memory.output, - weather: weather.output, - }); + const [output, setOutput] = createStore(providers.outputMap); - cpu.onOutput(cpu => setOutputs({ cpu })); - battery.onOutput(battery => setOutputs({ battery })); - memory.onOutput(memory => setOutputs({ memory })); - weather.onOutput(weather => setOutputs({ weather })); + providers.onOutput(outputMap => setOutput(outputMap)); return (
-
CPU usage: {outputs.cpu.usage}
+
CPU usage: {output.cpu.usage}
- Battery charge: {outputs.battery?.chargePercent} + Battery charge: {output.battery?.chargePercent}
-
Memory usage: {outputs.memory.usage}
-
Weather temp: {outputs.weather?.celsiusTemp}
+
Memory usage: {output.memory.usage}
+
Weather temp: {output.weather?.celsiusTemp}
); } diff --git a/examples/starter/vanilla.html b/examples/starter/vanilla.html index f3b21c80..2f7da58e 100644 --- a/examples/starter/vanilla.html +++ b/examples/starter/vanilla.html @@ -26,9 +26,9 @@ import { createRoot } from 'react-dom/client'; import { init } from 'zebar'; - const ctx = await init(); + const zebarCtx = await init(); - const providers = await ctx.createProviderGroup({ + const providers = await zebarCtx.createProviderGroup({ network: { type: 'network' }, cpu: { type: 'cpu' }, date: { type: 'date', formatting: 'EEE d MMM t' }, @@ -40,10 +40,10 @@ createRoot(document.getElementById('root')).render(); function App() { - const [output, setOutput] = useState(providers.outputs); + const [output, setOutput] = useState(providers.outputMap); useEffect(() => { - providers.onOutput(() => setOutput(providers.outputs)); + providers.onOutput(() => setOutput(providers.outputMap)); }, []); // Get icon to show for current network status. diff --git a/examples/starter/with-glazewm.html b/examples/starter/with-glazewm.html index 33e3c3bc..7ecb355a 100644 --- a/examples/starter/with-glazewm.html +++ b/examples/starter/with-glazewm.html @@ -28,72 +28,43 @@ const zebarCtx = await init(); - const [network, glazewm, cpu, date, battery, memory, weather] = - await Promise.all([ - zebarCtx.createProvider({ type: 'network' }), - zebarCtx.createProvider({ type: 'glazewm' }), - zebarCtx.createProvider({ type: 'cpu' }), - zebarCtx.createProvider({ - type: 'date', - formatting: 'EEE d MMM t', - }), - zebarCtx.createProvider({ type: 'battery' }), - zebarCtx.createProvider({ type: 'memory' }), - zebarCtx.createProvider({ type: 'weather' }), - ]); + const providers = await zebarCtx.createProviderGroup({ + network: { type: 'network' }, + glazewm: { type: 'glazewm' }, + cpu: { type: 'cpu' }, + date: { type: 'date', formatting: 'EEE d MMM t' }, + battery: { type: 'battery' }, + memory: { type: 'memory' }, + weather: { type: 'weather' }, + }); createRoot(document.getElementById('root')).render(); function App() { - const [outputs, setOutputs] = useState({ - network: network.output, - glazewm: glazewm.output, - cpu: cpu.output, - date: date.output, - battery: battery.output, - memory: memory.output, - weather: weather.output, - }); + const [output, setOutput] = useState(providers.outputMap); useEffect(() => { - network.onOutput(network => - setOutputs(outputs => ({ ...outputs, network })), - ); - glazewm.onOutput(glazewm => - setOutputs(outputs => ({ ...outputs, glazewm })), - ); - date.onOutput(date => - setOutputs(outputs => ({ ...outputs, date })), - ); - battery.onOutput(battery => - setOutputs(outputs => ({ ...outputs, battery })), - ); - memory.onOutput(memory => - setOutputs(outputs => ({ ...outputs, memory })), - ); - weather.onOutput(weather => - setOutputs(outputs => ({ ...outputs, weather })), - ); + providers.onOutput(() => setOutput(providers.outputMap)); }, []); // Get icon to show for current network status. function getNetworkIcon() { - switch (outputs.network.defaultInterface?.type) { + switch (output.network.defaultInterface?.type) { case 'ethernet': return ; case 'wifi': - if (outputs.network.defaultGateway?.signalStrength >= 80) { + if (output.network.defaultGateway?.signalStrength >= 80) { return ; } else if ( - outputs.network.defaultGateway?.signalStrength >= 65 + output.network.defaultGateway?.signalStrength >= 65 ) { return ; } else if ( - outputs.network.defaultGateway?.signalStrength >= 40 + output.network.defaultGateway?.signalStrength >= 40 ) { return ; } else if ( - outputs.network.defaultGateway?.signalStrength >= 25 + output.network.defaultGateway?.signalStrength >= 25 ) { return ; } else { @@ -108,20 +79,20 @@ // Get icon to show for how much of the battery is charged. function getBatteryIcon() { - if (outputs.battery.chargePercent > 90) + if (output.battery.chargePercent > 90) return ; - if (outputs.battery.chargePercent > 70) + if (output.battery.chargePercent > 70) return ; - if (outputs.battery.chargePercent > 40) + if (output.battery.chargePercent > 40) return ; - if (outputs.battery.chargePercent > 20) + if (output.battery.chargePercent > 20) return ; return ; } // Get icon to show for current weather status. function getWeatherIcon() { - switch (outputs.weather.status) { + switch (output.weather.status) { case 'clear_day': return ; case 'clear_night': @@ -153,13 +124,13 @@
- {outputs.glazewm && ( + {output.glazewm && (
- {outputs.glazewm.currentWorkspaces.map(workspace => ( + {output.glazewm.currentWorkspaces.map(workspace => (
-
{outputs.date.formatted}
+
{output.date.formatted}
- {outputs.glazewm && ( + {output.glazewm && ( <> - {outputs.glazewm.bindingModes.map(bindingMode => ( + {output.glazewm.bindingModes.map(bindingMode => ( )} - {outputs.network && ( + {output.network && (
{getNetworkIcon()} - {outputs.network.defaultGateway?.ssid} + {output.network.defaultGateway?.ssid}
)}
- {Math.round(outputs.memory.usage)}% + {Math.round(output.memory.usage)}%
@@ -212,27 +183,27 @@ {/* Change the text color if the CPU usage is high. */} 85 ? 'high-usage' : ''} + className={output.cpu.usage > 85 ? 'high-usage' : ''} > - {Math.round(outputs.cpu.usage)}% + {Math.round(output.cpu.usage)}%
- {outputs.battery && ( + {output.battery && (
{/* Show icon for whether battery is charging. */} - {outputs.battery.isCharging && ( + {output.battery.isCharging && ( )} {getBatteryIcon()} - {Math.round(outputs.battery.chargePercent)}% + {Math.round(output.battery.chargePercent)}%
)} - {outputs.weather && ( + {output.weather && (
{getWeatherIcon()} - {Math.round(outputs.weather.celsiusTemp)}°C + {Math.round(output.weather.celsiusTemp)}°C
)}
diff --git a/examples/starter/with-komorebi.html b/examples/starter/with-komorebi.html index 273f83be..adce2d05 100644 --- a/examples/starter/with-komorebi.html +++ b/examples/starter/with-komorebi.html @@ -28,75 +28,43 @@ const zebarCtx = await init(); - const [network, komorebi, cpu, date, battery, memory, weather] = - await Promise.all([ - zebarCtx.createProvider({ type: 'network' }), - zebarCtx.createProvider({ type: 'komorebi' }), - zebarCtx.createProvider({ type: 'cpu' }), - zebarCtx.createProvider({ - type: 'date', - formatting: 'EEE d MMM t', - }), - zebarCtx.createProvider({ type: 'battery' }), - zebarCtx.createProvider({ type: 'memory' }), - zebarCtx.createProvider({ type: 'weather' }), - ]); + const providers = await zebarCtx.createProviderGroup({ + network: { type: 'network' }, + komorebi: { type: 'komorebi' }, + cpu: { type: 'cpu' }, + date: { type: 'date', formatting: 'EEE d MMM t' }, + battery: { type: 'battery' }, + memory: { type: 'memory' }, + weather: { type: 'weather' }, + }); createRoot(document.getElementById('root')).render(); function App() { - const [outputs, setOutputs] = useState({ - network: network.output, - komorebi: komorebi.output, - cpu: cpu.output, - date: date.output, - battery: battery.output, - memory: memory.output, - weather: weather.output, - }); + const [output, setOutput] = useState(providers.outputMap); useEffect(() => { - network.onOutput(network => - setOutputs(outputs => ({ ...outputs, network })), - ); - komorebi.onOutput(komorebi => - setOutputs(outputs => ({ ...outputs, komorebi })), - ); - cpu.onOutput(cpu => - setOutputs(outputs => ({ ...outputs, cpu })), - ); - date.onOutput(date => - setOutputs(outputs => ({ ...outputs, date })), - ); - battery.onOutput(battery => - setOutputs(outputs => ({ ...outputs, battery })), - ); - memory.onOutput(memory => - setOutputs(outputs => ({ ...outputs, memory })), - ); - weather.onOutput(weather => - setOutputs(outputs => ({ ...outputs, weather })), - ); + providers.onOutput(() => setOutput(providers.outputMap)); }, []); // Get icon to show for current network status. function getNetworkIcon() { - switch (outputs.network.defaultInterface?.type) { + switch (output.network.defaultInterface?.type) { case 'ethernet': return ; case 'wifi': - if (outputs.network.defaultGateway?.signalStrength >= 80) { + if (output.network.defaultGateway?.signalStrength >= 80) { return ; } else if ( - outputs.network.defaultGateway?.signalStrength >= 65 + output.network.defaultGateway?.signalStrength >= 65 ) { return ; } else if ( - outputs.network.defaultGateway?.signalStrength >= 40 + output.network.defaultGateway?.signalStrength >= 40 ) { return ; } else if ( - outputs.network.defaultGateway?.signalStrength >= 25 + output.network.defaultGateway?.signalStrength >= 25 ) { return ; } else { @@ -111,20 +79,20 @@ // Get icon to show for how much of the battery is charged. function getBatteryIcon() { - if (outputs.battery.chargePercent > 90) + if (output.battery.chargePercent > 90) return ; - if (outputs.battery.chargePercent > 70) + if (output.battery.chargePercent > 70) return ; - if (outputs.battery.chargePercent > 40) + if (output.battery.chargePercent > 40) return ; - if (outputs.battery.chargePercent > 20) + if (output.battery.chargePercent > 20) return ; return ; } // Get icon to show for current weather status. function getWeatherIcon() { - switch (outputs.weather.status) { + switch (output.weather.status) { case 'clear_day': return ; case 'clear_night': @@ -157,11 +125,11 @@
- {outputs.komorebi && ( + {output.komorebi && (
- {outputs.komorebi.currentWorkspaces.map(workspace => ( + {output.komorebi.currentWorkspaces.map(workspace => (
-
{outputs.date.formatted}
+
{output.date.formatted}
- {outputs.network && ( + {output.network && (
{getNetworkIcon()} - {outputs.network.defaultGateway?.ssid} + {output.network.defaultGateway?.ssid}
)}
- {Math.round(outputs.memory.usage)}% + {Math.round(output.memory.usage)}%
@@ -191,27 +159,27 @@ {/* Change the text color if the CPU usage is high. */} 85 ? 'high-usage' : ''} + className={output.cpu.usage > 85 ? 'high-usage' : ''} > - {Math.round(outputs.cpu.usage)}% + {Math.round(output.cpu.usage)}%
- {outputs.battery && ( + {output.battery && (
{/* Show icon for whether battery is charging. */} - {outputs.battery.isCharging && ( + {output.battery.isCharging && ( )} {getBatteryIcon()} - {Math.round(outputs.battery.chargePercent)}% + {Math.round(output.battery.chargePercent)}%
)} - {outputs.weather && ( + {output.weather && (
{getWeatherIcon()} - {Math.round(outputs.weather.celsiusTemp)}°C + {Math.round(output.weather.celsiusTemp)}°C
)}
From c0e404749ccb28c95f6f8b3179a68741256fb01c Mon Sep 17 00:00:00 2001 From: CtByte <165908630+CtByte@users.noreply.github.com> Date: Fri, 13 Sep 2024 14:42:44 +0200 Subject: [PATCH 131/138] feat: v2 - added scale_factor to the window_factory (#101) --- packages/desktop/src/window_factory.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/desktop/src/window_factory.rs b/packages/desktop/src/window_factory.rs index 9769ccf6..82d8c8a6 100644 --- a/packages/desktop/src/window_factory.rs +++ b/packages/desktop/src/window_factory.rs @@ -10,7 +10,7 @@ use std::{ use anyhow::{bail, Context}; use serde::Serialize; use tauri::{ - AppHandle, Manager, WebviewUrl, WebviewWindowBuilder, WindowEvent, + AppHandle, Manager, WebviewUrl, WebviewWindowBuilder, WindowEvent, LogicalSize, PhysicalSize }; use tokio::{ sync::{broadcast, Mutex}, @@ -72,6 +72,7 @@ pub struct WindowPlacement { height: f64, x: f64, y: f64, + scale_factor: f64, } impl WindowFactory { @@ -134,6 +135,16 @@ impl WindowFactory { ) .into(), ); + + let size: LogicalSize = LogicalSize::from_physical( + PhysicalSize::new(placement.width, placement.height), + placement.scale_factor, + ); + + let position: LogicalSize = LogicalSize::from_physical( + PhysicalSize::new(placement.x, placement.y), + placement.scale_factor, + ); // Note that window label needs to be globally unique. let window = WebviewWindowBuilder::new( @@ -142,8 +153,8 @@ impl WindowFactory { webview_url, ) .title("Zebar") - .inner_size(placement.width, placement.height) - .position(placement.x, placement.y) + .inner_size(size.width, size.height) + .position(position.width, position.height) .focused(config.launch_options.focused) .skip_taskbar(!config.launch_options.shown_in_taskbar) .visible_on_all_workspaces(true) @@ -264,6 +275,7 @@ impl WindowFactory { as f64, y: (anchor_y + placement.offset_y.to_px(monitor.height as i32)) as f64, + scale_factor: monitor.scale_factor, }; placements.push(placement); From 71ced0b5f89606899f2819b3293ee9b9beeda0cd Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Fri, 13 Sep 2024 21:59:57 +0800 Subject: [PATCH 132/138] fix: handle constrained window size on initial launch --- packages/desktop/src/window_factory.rs | 66 +++++++++++++------------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/packages/desktop/src/window_factory.rs b/packages/desktop/src/window_factory.rs index 82d8c8a6..880aadc0 100644 --- a/packages/desktop/src/window_factory.rs +++ b/packages/desktop/src/window_factory.rs @@ -10,7 +10,8 @@ use std::{ use anyhow::{bail, Context}; use serde::Serialize; use tauri::{ - AppHandle, Manager, WebviewUrl, WebviewWindowBuilder, WindowEvent, LogicalSize, PhysicalSize + AppHandle, LogicalPosition, LogicalSize, Manager, PhysicalPosition, + PhysicalSize, WebviewUrl, WebviewWindowBuilder, WindowEvent, }; use tokio::{ sync::{broadcast, Mutex}, @@ -67,14 +68,6 @@ pub struct WindowState { pub html_path: PathBuf, } -pub struct WindowPlacement { - width: f64, - height: f64, - x: f64, - y: f64, - scale_factor: f64, -} - impl WindowFactory { /// Creates a new `WindowFactory` instance. pub fn new( @@ -107,7 +100,7 @@ impl WindowFactory { html_path, } = &config_entry; - for placement in self.window_placements(config) { + for (size, position) in self.window_placements(config) { // Use running window count as a unique ID for the window. let new_count = self.window_count.fetch_add(1, Ordering::Relaxed) + 1; @@ -135,16 +128,6 @@ impl WindowFactory { ) .into(), ); - - let size: LogicalSize = LogicalSize::from_physical( - PhysicalSize::new(placement.width, placement.height), - placement.scale_factor, - ); - - let position: LogicalSize = LogicalSize::from_physical( - PhysicalSize::new(placement.x, placement.y), - placement.scale_factor, - ); // Note that window label needs to be globally unique. let window = WebviewWindowBuilder::new( @@ -154,7 +137,7 @@ impl WindowFactory { ) .title("Zebar") .inner_size(size.width, size.height) - .position(position.width, position.height) + .position(position.x, position.y) .focused(config.launch_options.focused) .skip_taskbar(!config.launch_options.shown_in_taskbar) .visible_on_all_workspaces(true) @@ -176,14 +159,23 @@ impl WindowFactory { serde_json::to_string(&state)? )); - // Tauri's `skip_taskbar` option isn't 100% reliable, so we - // also set the window as a tool window. + // On Windows, Tauri's `skip_taskbar` option isn't 100% reliable, so + // we also set the window as a tool window. #[cfg(target_os = "windows")] let _ = window .as_ref() .window() .set_tool_window(!config.launch_options.shown_in_taskbar); + // On Windows, there's an issue where the window size is constrained + // when initially created. To work around this, apply the size and + // position settings again after launch. + #[cfg(target_os = "windows")] + { + let _ = window.set_size(size); + let _ = window.set_position(position); + } + let mut window_states = self.window_states.lock().await; window_states.insert(state.window_id.clone(), state.clone()); @@ -227,7 +219,7 @@ impl WindowFactory { fn window_placements( &self, config: &WindowConfig, - ) -> Vec { + ) -> Vec<(LogicalSize, LogicalPosition)> { let mut placements = vec![]; for placement in config.launch_options.placements.iter() { @@ -268,17 +260,23 @@ impl WindowFactory { ), }; - let placement = WindowPlacement { - width: placement.width.to_px(monitor.width as i32) as f64, - height: placement.height.to_px(monitor.height as i32) as f64, - x: (anchor_x + placement.offset_x.to_px(monitor.width as i32)) - as f64, - y: (anchor_y + placement.offset_y.to_px(monitor.height as i32)) - as f64, - scale_factor: monitor.scale_factor, - }; + let size = LogicalSize::from_physical( + PhysicalSize::new( + placement.width.to_px(monitor.width as i32), + placement.height.to_px(monitor.height as i32), + ), + monitor.scale_factor, + ); + + let position = LogicalPosition::from_physical( + PhysicalPosition::new( + anchor_x + placement.offset_x.to_px(monitor.width as i32), + anchor_y + placement.offset_y.to_px(monitor.height as i32), + ), + monitor.scale_factor, + ); - placements.push(placement); + placements.push((size, position)); } } From 01541ace2bff9bdd5391c44eac1b638d499f9a0b Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Sun, 15 Sep 2024 17:42:46 +0800 Subject: [PATCH 133/138] feat: implement config reloading; add option to tray menu --- packages/desktop/src/config.rs | 10 ++++ packages/desktop/src/main.rs | 78 ++++++++++++++++++++++---- packages/desktop/src/sys_tray.rs | 60 +++++++------------- packages/desktop/src/window_factory.rs | 30 +++++++++- 4 files changed, 124 insertions(+), 54 deletions(-) diff --git a/packages/desktop/src/config.rs b/packages/desktop/src/config.rs index da810f22..b42995ad 100644 --- a/packages/desktop/src/config.rs +++ b/packages/desktop/src/config.rs @@ -132,6 +132,10 @@ pub struct Config { _settings_change_rx: broadcast::Receiver, pub settings_change_tx: broadcast::Sender, + + _window_configs_change_rx: broadcast::Receiver>, + + pub window_configs_change_tx: broadcast::Sender>, } #[derive(Clone, Debug)] @@ -166,7 +170,10 @@ impl Config { let settings = Self::read_settings_or_init(app_handle, &config_dir)?; let window_configs = Self::read_window_configs(&config_dir)?; + let (settings_change_tx, _settings_change_rx) = broadcast::channel(16); + let (window_configs_change_tx, _window_configs_change_rx) = + broadcast::channel(16); Ok(Self { app_handle: app_handle.clone(), @@ -175,6 +182,8 @@ impl Config { window_configs: Arc::new(Mutex::new(window_configs)), _settings_change_rx, settings_change_tx, + _window_configs_change_rx, + window_configs_change_tx, }) } @@ -195,6 +204,7 @@ impl Config { } self.settings_change_tx.send(new_settings)?; + self.window_configs_change_tx.send(new_window_configs)?; Ok(()) } diff --git a/packages/desktop/src/main.rs b/packages/desktop/src/main.rs index a71f16e5..adb578ea 100644 --- a/packages/desktop/src/main.rs +++ b/packages/desktop/src/main.rs @@ -3,13 +3,13 @@ #![feature(async_closure)] #![feature(iterator_try_collect)] -use std::{env, sync::Arc, time::Duration}; +use std::{env, sync::Arc}; use anyhow::Context; use clap::Parser; -use tauri::{async_runtime::block_on, Manager}; -use tokio::{task, time::sleep}; -use tracing::{error, level_filters::LevelFilter}; +use tauri::{async_runtime::block_on, Manager, RunEvent}; +use tokio::task; +use tracing::{error, info, level_filters::LevelFilter}; use tracing_subscriber::EnvFilter; use crate::{ @@ -86,7 +86,7 @@ fn start_app(cli: Cli) -> anyhow::Result<()> { tauri::async_runtime::set(tokio::runtime::Handle::current()); - tauri::Builder::default() + let app = tauri::Builder::default() .setup(move |app| { task::block_in_place(|| { block_on(async move { @@ -106,8 +106,11 @@ fn start_app(cli: Cli) -> anyhow::Result<()> { app.manage(monitor_state.clone()); // Initialize `WindowFactory` in Tauri state. - let window_factory = - Arc::new(WindowFactory::new(app.handle(), monitor_state)); + let window_factory = Arc::new(WindowFactory::new( + app.handle(), + config.clone(), + monitor_state, + )); app.manage(window_factory.clone()); // If this is not the first instance of the app, this will emit @@ -145,9 +148,14 @@ fn start_app(cli: Cli) -> anyhow::Result<()> { .await?; // Add application icon to system tray. - let _ = - SysTray::new(app.handle(), config.clone(), window_factory) - .await?; + let tray = SysTray::new( + app.handle(), + config.clone(), + window_factory.clone(), + ) + .await?; + + listen_events(window_factory, config, tray); Ok(()) }) @@ -161,11 +169,59 @@ fn start_app(cli: Cli) -> anyhow::Result<()> { commands::set_always_on_top, commands::set_skip_taskbar ]) - .run(tauri::generate_context!())?; + .build(tauri::generate_context!())?; + + app.run(|_, event| { + if let RunEvent::ExitRequested { code, api, .. } = &event { + if code.is_none() { + // Keep the message loop running even if all windows are closed. + api.prevent_exit(); + } + } + }); Ok(()) } +fn listen_events( + window_factory: Arc, + config: Arc, + tray: Arc, +) { + let mut window_open_rx = window_factory.open_tx.subscribe(); + let mut window_close_rx = window_factory.close_tx.subscribe(); + let mut settings_change_rx = config.settings_change_tx.subscribe(); + let mut window_configs_change_rx = + config.window_configs_change_tx.subscribe(); + + task::spawn(async move { + loop { + let res = tokio::select! { + Ok(_) = window_open_rx.recv() => { + info!("Window opened."); + tray.refresh().await + }, + Ok(_) = window_close_rx.recv() => { + info!("Window closed."); + tray.refresh().await + }, + Ok(_) = settings_change_rx.recv() => { + info!("Settings changed."); + tray.refresh().await + }, + Ok(_) = window_configs_change_rx.recv() => { + info!("Window configs changed."); + window_factory.relaunch_all().await + }, + }; + + if let Err(err) = res { + error!("{:?}", err); + } + } + }); +} + /// Setup single instance Tauri plugin. fn setup_single_instance( app: &tauri::App, diff --git a/packages/desktop/src/sys_tray.rs b/packages/desktop/src/sys_tray.rs index 9107940a..84d619d1 100644 --- a/packages/desktop/src/sys_tray.rs +++ b/packages/desktop/src/sys_tray.rs @@ -19,6 +19,7 @@ use crate::{ #[derive(Debug, Clone)] enum MenuEvent { ShowConfigFolder, + ReloadConfigs, Exit, ToggleWindowConfig { enable: bool, path: PathBuf }, ToggleStartupWindowConfig { enable: bool, path: PathBuf }, @@ -28,6 +29,7 @@ impl ToString for MenuEvent { fn to_string(&self) -> String { match self { MenuEvent::ShowConfigFolder => "show_config_folder".to_string(), + MenuEvent::ReloadConfigs => "reload_configs".to_string(), MenuEvent::Exit => "exit".to_string(), MenuEvent::ToggleWindowConfig { enable, path } => { format!( @@ -55,6 +57,7 @@ impl FromStr for MenuEvent { match parts.as_slice() { ["show", "config", "folder"] => Ok(Self::ShowConfigFolder), + ["reload", "configs"] => Ok(Self::ReloadConfigs), ["exit"] => Ok(Self::Exit), ["toggle", "window", "config", enable @ ("true" | "false"), path @ ..] => { Ok(Self::ToggleWindowConfig { @@ -97,10 +100,7 @@ impl SysTray { sys_tray.tray_icon = Some(sys_tray.create_tray_icon().await?); - let sys_tray = Arc::new(sys_tray); - sys_tray.clone().start_listener(); - - Ok(sys_tray) + Ok(Arc::new(sys_tray)) } async fn create_tray_icon(&self) -> anyhow::Result { @@ -142,43 +142,15 @@ impl SysTray { Ok(tray_icon) } - fn start_listener(self: Arc) { - let mut window_open_rx = self.window_factory.open_tx.subscribe(); - let mut window_close_rx = self.window_factory.close_tx.subscribe(); - let mut settings_change_rx = - self.config.settings_change_tx.subscribe(); - - tokio::spawn(async move { - loop { - let event = tokio::select! { - Ok(_) = window_open_rx.recv() => "Window open", - Ok(_) = window_close_rx.recv() => "Window close", - Ok(_) = settings_change_rx.recv() => "Settings change", - }; - - info!("{} received in system tray. Updating menu.", event); - - // Drain receiver channels if multiple events are queued up. - window_open_rx = window_open_rx.resubscribe(); - window_close_rx = window_close_rx.resubscribe(); - settings_change_rx = settings_change_rx.resubscribe(); - - if let Some(tray_icon) = self.tray_icon.as_ref() { - if let Err(err) = - self.create_tray_menu().await.and_then(|menu| { - tray_icon - .set_menu(Some(menu)) - .context("Failed to set tray menu.") - }) - { - error!( - "Failed to update tray menu after {} event: {:?}", - event, err - ); - } - } - } - }); + pub async fn refresh(&self) -> anyhow::Result<()> { + info!("Updating system tray menu."); + + if let Some(tray_icon) = self.tray_icon.as_ref() { + let tray_menu = self.create_tray_menu().await?; + tray_icon.set_menu(Some(tray_menu))?; + } + + Ok(()) } /// Returns the image to use for the system tray icon. @@ -209,6 +181,7 @@ impl SysTray { let mut tray_menu = MenuBuilder::new(&self.app_handle) .item(&configs_menu) .text(MenuEvent::ShowConfigFolder, "Show config folder") + .text(MenuEvent::ReloadConfigs, "Reload configs") .separator(); // Add submenus for currently active windows. @@ -251,6 +224,11 @@ impl SysTray { .open_config_dir() .context("Failed to open config folder.") } + MenuEvent::ReloadConfigs => { + info!("Opening config folder from system tray."); + + config.reload().await + } MenuEvent::Exit => { info!("Exiting through system tray."); diff --git a/packages/desktop/src/window_factory.rs b/packages/desktop/src/window_factory.rs index 880aadc0..f8b2ff0f 100644 --- a/packages/desktop/src/window_factory.rs +++ b/packages/desktop/src/window_factory.rs @@ -21,7 +21,7 @@ use tracing::{error, info}; use crate::{ common::{PathExt, WindowExt}, - config::{WindowAnchor, WindowConfig, WindowConfigEntry}, + config::{Config, WindowAnchor, WindowConfig, WindowConfigEntry}, monitor_state::MonitorState, }; @@ -34,11 +34,16 @@ pub struct WindowFactory { pub close_tx: broadcast::Sender, + /// Reference to `Config`. + config: Arc, + _open_rx: broadcast::Receiver, pub open_tx: broadcast::Sender, - /// Reference to `MonitorState` for window positioning. + /// Reference to `MonitorState`. + /// + /// Used for window positioning. monitor_state: Arc, /// Running total of windows created. @@ -72,6 +77,7 @@ impl WindowFactory { /// Creates a new `WindowFactory` instance. pub fn new( app_handle: &AppHandle, + config: Arc, monitor_state: Arc, ) -> Self { let (open_tx, _open_rx) = broadcast::channel(16); @@ -81,6 +87,7 @@ impl WindowFactory { app_handle: app_handle.clone(), _close_rx, close_tx, + config, _open_rx, open_tx, monitor_state, @@ -313,6 +320,25 @@ impl WindowFactory { Ok(()) } + /// Relaunches all currently open windows. + pub async fn relaunch_all(&self) -> anyhow::Result<()> { + let window_states = self.states_by_config_path().await; + + for (config_path, _) in window_states { + let _ = self.close_by_path(&config_path).await; + + let window_config = self + .config + .window_config_by_path(&config_path) + .await? + .context("Window config not found.")?; + + self.open(window_config).await?; + } + + Ok(()) + } + /// Returns window state by a given window ID. pub async fn state_by_id(&self, window_id: &str) -> Option { self.window_states.lock().await.get(window_id).cloned() From 51a58df2367717cbf94b4f11cb0889fc54db3fb3 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Mon, 16 Sep 2024 00:21:16 +0800 Subject: [PATCH 134/138] feat: relaunch windows on display setting changes --- packages/desktop/src/main.rs | 12 +++- packages/desktop/src/monitor_state.rs | 91 ++++++++++++++++++++------ packages/desktop/src/window_factory.rs | 7 +- 3 files changed, 85 insertions(+), 25 deletions(-) diff --git a/packages/desktop/src/main.rs b/packages/desktop/src/main.rs index adb578ea..18a2ae1d 100644 --- a/packages/desktop/src/main.rs +++ b/packages/desktop/src/main.rs @@ -109,7 +109,7 @@ fn start_app(cli: Cli) -> anyhow::Result<()> { let window_factory = Arc::new(WindowFactory::new( app.handle(), config.clone(), - monitor_state, + monitor_state.clone(), )); app.manage(window_factory.clone()); @@ -155,7 +155,7 @@ fn start_app(cli: Cli) -> anyhow::Result<()> { ) .await?; - listen_events(window_factory, config, tray); + listen_events(config, monitor_state, window_factory, tray); Ok(()) }) @@ -184,13 +184,15 @@ fn start_app(cli: Cli) -> anyhow::Result<()> { } fn listen_events( - window_factory: Arc, config: Arc, + monitor_state: Arc, + window_factory: Arc, tray: Arc, ) { let mut window_open_rx = window_factory.open_tx.subscribe(); let mut window_close_rx = window_factory.close_tx.subscribe(); let mut settings_change_rx = config.settings_change_tx.subscribe(); + let mut monitors_change_rx = monitor_state.change_tx.subscribe(); let mut window_configs_change_rx = config.window_configs_change_tx.subscribe(); @@ -209,6 +211,10 @@ fn listen_events( info!("Settings changed."); tray.refresh().await }, + Ok(_) = monitors_change_rx.recv() => { + info!("Monitors changed."); + window_factory.relaunch_all().await + }, Ok(_) = window_configs_change_rx.recv() => { info!("Window configs changed."); window_factory.relaunch_all().await diff --git a/packages/desktop/src/monitor_state.rs b/packages/desktop/src/monitor_state.rs index 669d020f..79ba359e 100644 --- a/packages/desktop/src/monitor_state.rs +++ b/packages/desktop/src/monitor_state.rs @@ -1,5 +1,12 @@ +use std::sync::Arc; + use anyhow::bail; use tauri::AppHandle; +use tokio::{ + sync::{broadcast, Mutex}, + task, +}; +use tracing::info; use crate::{cli::OutputMonitorsArgs, config::MonitorSelection}; @@ -7,10 +14,15 @@ pub struct MonitorState { /// Handle to the Tauri application. app_handle: AppHandle, + _change_rx: broadcast::Receiver>, + + pub change_tx: broadcast::Sender>, + /// Available monitors sorted from left-to-right and top-to-bottom. - monitors: Vec, + monitors: Arc>>, } +#[derive(Clone, Debug, PartialEq)] pub struct Monitor { pub name: Option, pub is_primary: bool, @@ -24,15 +36,55 @@ pub struct Monitor { impl MonitorState { /// Creates a new `MonitorState` instance. pub fn new(app_handle: &AppHandle) -> Self { + let (change_tx, _change_rx) = broadcast::channel(16); + + let monitors = + Arc::new(Mutex::new(Self::available_monitors(app_handle))); + + Self::listen_changes( + app_handle.clone(), + monitors.clone(), + change_tx.clone(), + ); + Self { app_handle: app_handle.clone(), - monitors: Self::monitors(app_handle), + monitors, + _change_rx, + change_tx, } } - /// Returns a vector of available monitors sorted from left-to-right and + /// Listens for display setting changes. + /// + /// Updates monitor state on scaling changes, monitor connections, and + /// monitor disconnections. Does not update on working area changes. + fn listen_changes( + app_handle: AppHandle, + monitors: Arc>>, + change_tx: broadcast::Sender>, + ) { + task::spawn(async move { + loop { + let new_monitors = Self::available_monitors(&app_handle); + let mut monitors = monitors.lock().await; + + if *monitors != new_monitors { + info!("Detected change in monitors."); + let _ = change_tx.send(new_monitors.clone()); + *monitors = new_monitors; + } + + tokio::time::sleep(std::time::Duration::from_secs(4)).await; + } + }); + } + + /// Gets available monitors on the system. + /// + /// Returns a vector of `Monitor` instances sorted from left-to-right and /// top-to-bottom. - fn monitors(app_handle: &AppHandle) -> Vec { + fn available_monitors(app_handle: &AppHandle) -> Vec { let primary_monitor = app_handle.primary_monitor().unwrap_or(None); let mut monitors = app_handle @@ -73,13 +125,15 @@ impl MonitorState { &self, args: OutputMonitorsArgs, ) -> anyhow::Result { - if self.monitors.len() == 0 { + let monitors = self.monitors.try_lock()?; + + if monitors.len() == 0 { bail!("No monitors found") } let mut monitors_str = String::new(); - for monitor in &self.monitors { + for monitor in monitors.iter() { monitors_str += &format!( "MONITOR_NAME=\"{}\" MONITOR_X=\"{}\" MONITOR_Y=\"{}\" MONITOR_WIDTH=\"{}\" MONITOR_HEIGHT=\"{}\" MONITOR_SCALE_FACTOR=\"{}\"", monitor.name.clone().unwrap_or("".into()), @@ -99,28 +153,27 @@ impl MonitorState { Ok(monitors_str) } - pub fn monitors_by_selection( + pub async fn monitors_by_selection( &self, monitor_selection: &MonitorSelection, - ) -> Vec<&Monitor> { + ) -> Vec { + let monitors = self.monitors.lock().await.clone(); + match monitor_selection { - MonitorSelection::All => self.monitors.iter().collect(), - MonitorSelection::Primary => self - .monitors - .iter() + MonitorSelection::All => monitors, + MonitorSelection::Primary => monitors + .into_iter() .filter(|monitor| monitor.is_primary) .collect(), - MonitorSelection::Secondary => self - .monitors - .iter() + MonitorSelection::Secondary => monitors + .into_iter() .filter(|monitor| !monitor.is_primary) .collect(), MonitorSelection::Index(index) => { - self.monitors.get(*index).into_iter().collect() + monitors.get(*index).cloned().into_iter().collect() } - MonitorSelection::Name(name) => self - .monitors - .iter() + MonitorSelection::Name(name) => monitors + .into_iter() .filter(|monitor| monitor.name.as_deref() == Some(name)) .collect(), } diff --git a/packages/desktop/src/window_factory.rs b/packages/desktop/src/window_factory.rs index f8b2ff0f..0d55922f 100644 --- a/packages/desktop/src/window_factory.rs +++ b/packages/desktop/src/window_factory.rs @@ -107,7 +107,7 @@ impl WindowFactory { html_path, } = &config_entry; - for (size, position) in self.window_placements(config) { + for (size, position) in self.window_placements(config).await { // Use running window count as a unique ID for the window. let new_count = self.window_count.fetch_add(1, Ordering::Relaxed) + 1; @@ -223,7 +223,7 @@ impl WindowFactory { } /// Returns coordinates for window placement based on the given config. - fn window_placements( + async fn window_placements( &self, config: &WindowConfig, ) -> Vec<(LogicalSize, LogicalPosition)> { @@ -232,7 +232,8 @@ impl WindowFactory { for placement in config.launch_options.placements.iter() { let monitors = self .monitor_state - .monitors_by_selection(&placement.monitor_selection); + .monitors_by_selection(&placement.monitor_selection) + .await; for monitor in monitors { let (anchor_x, anchor_y) = match placement.anchor { From 6decc2eddd75f6d6bd9ba2813ad8d13c4369aaf6 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Mon, 16 Sep 2024 02:56:50 +0800 Subject: [PATCH 135/138] fix: skip missed tickets in interval provider --- packages/desktop/src/providers/provider.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/desktop/src/providers/provider.rs b/packages/desktop/src/providers/provider.rs index 47887d28..f751662a 100644 --- a/packages/desktop/src/providers/provider.rs +++ b/packages/desktop/src/providers/provider.rs @@ -33,6 +33,11 @@ macro_rules! impl_interval_provider { std::time::Duration::from_millis(self.refresh_interval_ms()), ); + // Skip missed ticks when the interval runs. This prevents a burst + // of backlogged ticks after a delay. + interval + .set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + loop { interval.tick().await; From 46bc6e5cafcc73bf8925867c4690157d4f83a86b Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Mon, 16 Sep 2024 13:58:18 +0800 Subject: [PATCH 136/138] feat: progress on byte formatting of network provider --- .../desktop/src/providers/network/provider.rs | 94 ++++++++++++------- .../src/providers/network/variables.rs | 36 +++++++ 2 files changed, 95 insertions(+), 35 deletions(-) diff --git a/packages/desktop/src/providers/network/provider.rs b/packages/desktop/src/providers/network/provider.rs index 5d696ee7..f4494927 100644 --- a/packages/desktop/src/providers/network/provider.rs +++ b/packages/desktop/src/providers/network/provider.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use anyhow::bail; use netdev::interface::get_interfaces; use sysinfo::Networks; use tokio::sync::Mutex; @@ -33,7 +34,6 @@ impl NetworkProvider { netinfo.refresh(); let interfaces = get_interfaces(); - let default_interface = netdev::get_default_interface().ok(); Ok(ProviderOutput::Network(NetworkOutput { @@ -52,18 +52,67 @@ impl NetworkProvider { .map(Self::transform_interface) .collect(), traffic: NetworkTraffic { - received: to_bytes_per_seconds( - get_network_down(&netinfo), - self.config.refresh_interval, - ), - transmitted: to_bytes_per_seconds( - get_network_up(&netinfo), - self.config.refresh_interval, - ), + received: Self::total_bytes_received(&netinfo) + / self.config.refresh_interval + * 1000, + transmitted: Self::total_bytes_transmitted(&netinfo) + / self.config.refresh_interval + * 1000, }, })) } + /// Gets the total network (down) usage. + /// + /// Returns the total bytes received by every network interface. + fn total_bytes_received(networks: &sysinfo::Networks) -> u64 { + let mut received_total: Vec = Vec::new(); + + for (_interface_name, network) in networks { + received_total.push(network.received()); + } + + received_total.iter().sum() + } + + /// Gets the total network (up) usage. + /// + /// Returns the total bytes transmitted by every network interface. + fn total_bytes_transmitted(networks: &sysinfo::Networks) -> u64 { + let mut transmitted_total: Vec = Vec::new(); + + for (_interface_name, network) in networks { + transmitted_total.push(network.transmitted()); + } + + transmitted_total.iter().sum() + } + + fn to_pretty_bytes(bytes: u64) -> anyhow::Result { + let magnitude = match bytes { + 0 => 0, + _ => bytes.ilog(1024), + }; + + let unit = match magnitude { + 0 => "B", + 1 => "KiB", + 2 => "MiB", + 3 => "GiB", + 4 => "TiB", + 5 => "PiB", + 6 => "EiB", + 7 => "ZiB", + 8 => "YiB", + _ => bail!("Unknown data unit"), + }; + + let result = bytes / ((1u64) << (magnitude * 10)); + + Ok(format!("{result:.1} {unit}").into()) + } + + /// Transforms a `netdev::Interface` into a `NetworkInterface`. fn transform_interface( interface: &netdev::Interface, ) -> NetworkInterface { @@ -94,6 +143,7 @@ impl NetworkProvider { } } + /// Transforms a `netdev::NetworkDevice` into a `NetworkGateway`. fn transform_gateway( gateway: &netdev::NetworkDevice, wifi_hotspot: WifiHotstop, @@ -117,29 +167,3 @@ impl NetworkProvider { } impl_interval_provider!(NetworkProvider); - -/// Gets the total network (down) usage. -fn get_network_down(req_net: &sysinfo::Networks) -> u64 { - // Get the total bytes recieved by every network interface - let mut received_total: Vec = Vec::new(); - for (_interface_name, network) in req_net { - received_total.push(network.received() as u64); - } - - received_total.iter().sum() -} - -/// Gets the total network (up) usage. -fn get_network_up(req_net: &sysinfo::Networks) -> u64 { - // Get the total bytes recieved by every network interface - let mut transmitted_total: Vec = Vec::new(); - for (_interface_name, network) in req_net { - transmitted_total.push(network.transmitted() as u64); - } - - transmitted_total.iter().sum() -} - -fn to_bytes_per_seconds(input_in_bytes: u64, timespan_in_ms: u64) -> u64 { - input_in_bytes / (timespan_in_ms / 1000) -} diff --git a/packages/desktop/src/providers/network/variables.rs b/packages/desktop/src/providers/network/variables.rs index eeb013f3..27e2d607 100644 --- a/packages/desktop/src/providers/network/variables.rs +++ b/packages/desktop/src/providers/network/variables.rs @@ -17,6 +17,42 @@ pub struct NetworkTraffic { pub transmitted: u64, } +#[derive(Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct NetworkTrafficMeasure { + pub bytes: u64, + pub si_value: u64, + pub si_unit: SiDataUnit, + pub iec_value: u64, + pub iec_unit: IecDataUnit, +} + +#[derive(Serialize, Debug, Clone)] +pub enum SiDataUnit { + B, + KB, + MB, + GB, + TB, + PB, + EB, + ZB, + YB, +} + +#[derive(Serialize, Debug, Clone)] +pub enum IecDataUnit { + B, + KiB, + MiB, + GiB, + TiB, + PiB, + EiB, + ZiB, + YiB, +} + #[derive(Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct NetworkInterface { From 6e7a0ea4995bc81d944301a9f23bb1209f343858 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Mon, 16 Sep 2024 15:18:19 +0800 Subject: [PATCH 137/138] feat: add formatted network traffic measure --- .../network/create-network-provider.ts | 12 +++- packages/desktop/src/common/format_bytes.rs | 43 ++++++++++++ packages/desktop/src/common/mod.rs | 2 + .../desktop/src/providers/network/provider.rs | 67 ++++++++++--------- .../src/providers/network/variables.rs | 38 ++--------- 5 files changed, 95 insertions(+), 67 deletions(-) create mode 100644 packages/desktop/src/common/format_bytes.rs diff --git a/packages/client-api/src/providers/network/create-network-provider.ts b/packages/client-api/src/providers/network/create-network-provider.ts index 86a1d6b3..012faaf4 100644 --- a/packages/client-api/src/providers/network/create-network-provider.ts +++ b/packages/client-api/src/providers/network/create-network-provider.ts @@ -74,8 +74,16 @@ export enum InterfaceType { } export interface NetworkTraffic { - received: number | null; - transmitted: number | null; + received: NetworkTrafficMeasure; + transmitted: NetworkTrafficMeasure; +} + +export interface NetworkTrafficMeasure { + bytes: number; + siValue: number; + siUnit: string; + iecValue: number; + iecUnit: string; } export async function createNetworkProvider( diff --git a/packages/desktop/src/common/format_bytes.rs b/packages/desktop/src/common/format_bytes.rs new file mode 100644 index 00000000..32afcf76 --- /dev/null +++ b/packages/desktop/src/common/format_bytes.rs @@ -0,0 +1,43 @@ +const SI_UNITS: [&'static str; 9] = + ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; + +const IEC_UNITS: [&'static str; 9] = + ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]; + +/// Converts a byte value to its SI (decimal) representation. +/// +/// Returns a tuple of the value and the SI unit as a string. +pub fn to_si_bytes(bytes: f64) -> (f64, String) { + if bytes < 1. && bytes > -1. { + return (bytes, "B".into()); + } + + let exponent = std::cmp::min( + (bytes.abs().log10() / 3.).floor() as i32, + (SI_UNITS.len() - 1) as i32, + ); + + ( + bytes / 1000f64.powi(exponent), + SI_UNITS[exponent as usize].into(), + ) +} + +/// Converts a byte value to its IEC (binary) representation. +/// +/// Returns a tuple of the value and the IEC unit as a string. +pub fn to_iec_bytes(bytes: f64) -> (f64, String) { + if bytes <= 1. && bytes >= -1. { + return (bytes, "B".into()); + } + + let exponent = std::cmp::min( + (bytes.abs().log2() / 10.).floor() as i32, + (IEC_UNITS.len() - 1) as i32, + ); + + ( + bytes / 1024f64.powi(exponent), + IEC_UNITS[exponent as usize].into(), + ) +} diff --git a/packages/desktop/src/common/mod.rs b/packages/desktop/src/common/mod.rs index c47493f2..328132b3 100644 --- a/packages/desktop/src/common/mod.rs +++ b/packages/desktop/src/common/mod.rs @@ -1,8 +1,10 @@ +mod format_bytes; mod fs_util; mod length_value; mod path_ext; mod window_ext; +pub use format_bytes::*; pub use fs_util::*; pub use length_value::*; pub use path_ext::*; diff --git a/packages/desktop/src/providers/network/provider.rs b/packages/desktop/src/providers/network/provider.rs index f4494927..2e70e113 100644 --- a/packages/desktop/src/providers/network/provider.rs +++ b/packages/desktop/src/providers/network/provider.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use anyhow::bail; +use anyhow::Context; use netdev::interface::get_interfaces; use sysinfo::Networks; use tokio::sync::Mutex; @@ -8,9 +8,13 @@ use tokio::sync::Mutex; use super::{ wifi_hotspot::{default_gateway_wifi, WifiHotstop}, InterfaceType, NetworkGateway, NetworkInterface, NetworkOutput, - NetworkProviderConfig, NetworkTraffic, + NetworkProviderConfig, NetworkTraffic, NetworkTrafficMeasure, +}; +use crate::{ + common::{to_iec_bytes, to_si_bytes}, + impl_interval_provider, + providers::ProviderOutput, }; -use crate::{impl_interval_provider, providers::ProviderOutput}; pub struct NetworkProvider { config: NetworkProviderConfig, @@ -36,6 +40,14 @@ impl NetworkProvider { let interfaces = get_interfaces(); let default_interface = netdev::get_default_interface().ok(); + let received_per_sec = Self::total_bytes_received(&netinfo) + / self.config.refresh_interval + * 1000; + + let transmitted_per_sec = Self::total_bytes_transmitted(&netinfo) + / self.config.refresh_interval + * 1000; + Ok(ProviderOutput::Network(NetworkOutput { default_interface: default_interface .as_ref() @@ -52,16 +64,29 @@ impl NetworkProvider { .map(Self::transform_interface) .collect(), traffic: NetworkTraffic { - received: Self::total_bytes_received(&netinfo) - / self.config.refresh_interval - * 1000, - transmitted: Self::total_bytes_transmitted(&netinfo) - / self.config.refresh_interval - * 1000, + received: Self::to_network_traffic_measure(received_per_sec)?, + transmitted: Self::to_network_traffic_measure( + transmitted_per_sec, + )?, }, })) } + fn to_network_traffic_measure( + bytes: u64, + ) -> anyhow::Result { + let (si_value, si_unit) = to_si_bytes(bytes as f64); + let (iec_value, iec_unit) = to_iec_bytes(bytes as f64); + + Ok(NetworkTrafficMeasure { + bytes, + si_value, + si_unit, + iec_value, + iec_unit, + }) + } + /// Gets the total network (down) usage. /// /// Returns the total bytes received by every network interface. @@ -88,30 +113,6 @@ impl NetworkProvider { transmitted_total.iter().sum() } - fn to_pretty_bytes(bytes: u64) -> anyhow::Result { - let magnitude = match bytes { - 0 => 0, - _ => bytes.ilog(1024), - }; - - let unit = match magnitude { - 0 => "B", - 1 => "KiB", - 2 => "MiB", - 3 => "GiB", - 4 => "TiB", - 5 => "PiB", - 6 => "EiB", - 7 => "ZiB", - 8 => "YiB", - _ => bail!("Unknown data unit"), - }; - - let result = bytes / ((1u64) << (magnitude * 10)); - - Ok(format!("{result:.1} {unit}").into()) - } - /// Transforms a `netdev::Interface` into a `NetworkInterface`. fn transform_interface( interface: &netdev::Interface, diff --git a/packages/desktop/src/providers/network/variables.rs b/packages/desktop/src/providers/network/variables.rs index 27e2d607..46979e6f 100644 --- a/packages/desktop/src/providers/network/variables.rs +++ b/packages/desktop/src/providers/network/variables.rs @@ -13,44 +13,18 @@ pub struct NetworkOutput { #[derive(Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct NetworkTraffic { - pub received: u64, - pub transmitted: u64, + pub received: NetworkTrafficMeasure, + pub transmitted: NetworkTrafficMeasure, } #[derive(Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct NetworkTrafficMeasure { pub bytes: u64, - pub si_value: u64, - pub si_unit: SiDataUnit, - pub iec_value: u64, - pub iec_unit: IecDataUnit, -} - -#[derive(Serialize, Debug, Clone)] -pub enum SiDataUnit { - B, - KB, - MB, - GB, - TB, - PB, - EB, - ZB, - YB, -} - -#[derive(Serialize, Debug, Clone)] -pub enum IecDataUnit { - B, - KiB, - MiB, - GiB, - TiB, - PiB, - EiB, - ZiB, - YiB, + pub si_value: f64, + pub si_unit: String, + pub iec_value: f64, + pub iec_unit: String, } #[derive(Serialize, Debug, Clone)] From bbe022991e774a84683874d285694297b6344d59 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Tue, 17 Sep 2024 14:58:02 +0800 Subject: [PATCH 138/138] chore: remove unused tauri capability permissions --- packages/desktop/capabilities/main.json | 4 +--- packages/desktop/tauri.conf.json | 5 ----- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/desktop/capabilities/main.json b/packages/desktop/capabilities/main.json index 316e9632..2ce37650 100644 --- a/packages/desktop/capabilities/main.json +++ b/packages/desktop/capabilities/main.json @@ -24,8 +24,6 @@ "window:allow-set-always-on-top", "window:allow-set-resizable", "window:allow-set-position", - "window:allow-set-size", - "shell:allow-open", - "tray:default" + "window:allow-set-size" ] } diff --git a/packages/desktop/tauri.conf.json b/packages/desktop/tauri.conf.json index 46b4603f..b67d233e 100644 --- a/packages/desktop/tauri.conf.json +++ b/packages/desktop/tauri.conf.json @@ -7,11 +7,6 @@ "productName": "Zebar", "version": "0.0.0", "identifier": "com.glzr.zebar", - "plugins": { - "shell": { - "open": true - } - }, "bundle": { "active": true, "icon": [