Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve error handling; show error dialogs #27

Merged
merged 3 commits into from
Feb 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/client-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
},
"dependencies": {
"@tauri-apps/api": "2.0.0-alpha.13",
"@tauri-apps/plugin-dialog": "2.0.0-alpha.5",
"@tauri-apps/plugin-shell": "2.0.0-alpha.3",
"@tauri-apps/plugin-window": "2.0.0-alpha.1",
"glazewm": "1.1.2",
Expand Down
145 changes: 81 additions & 64 deletions packages/client-api/src/init-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
runWithOwner,
} from 'solid-js';
import { createStore } from 'solid-js/store';
import { message as messageDialog } from '@tauri-apps/plugin-dialog';

import {
getStyleBuilder,
Expand All @@ -15,7 +16,9 @@ import {
import { getElementProviders } from './providers';
import type { ElementContext } from './element-context.model';
import { ElementType } from './element-type.model';
import type { PickPartial } from './utils';
import { createLogger, type PickPartial } from './utils';

const logger = createLogger('init-element');

export interface InitElementArgs {
id: string;
Expand All @@ -31,74 +34,88 @@ export interface InitElementArgs {
export async function initElement(
args: InitElementArgs,
): Promise<ElementContext> {
const styleBuilder = getStyleBuilder(args.owner);
const childConfigs = getChildConfigs(args.rawConfig);

// Create partial element context; `providers` and `parsedConfig` are set later.
const [elementContext, setElementContext] = createStore<
PickPartial<ElementContext, 'parsedConfig' | 'providers'>
>({
id: args.id,
type: args.type,
rawConfig: args.rawConfig,
globalConfig: args.globalConfig,
args: args.args,
env: args.env,
initChildElement,
providers: undefined,
parsedConfig: undefined,
});

const { element, merged } = await getElementProviders(
elementContext,
args.ancestorProviders,
args.owner,
);

setElementContext('providers', merged);

const parsedConfig = getParsedElementConfig(
elementContext as PickPartial<ElementContext, 'parsedConfig'>,
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);

runWithOwner(args.owner, () => {
createEffect(() => {
if (parsedConfig.styles) {
styleBuilder.setElementStyles(
parsedConfig.id,
parsedConfig.styles,
);
}
try {
const styleBuilder = getStyleBuilder(args.owner);
const childConfigs = getChildConfigs(args.rawConfig);

// Create partial element context; `providers` and `parsedConfig` are set later.
const [elementContext, setElementContext] = createStore<
PickPartial<ElementContext, 'parsedConfig' | 'providers'>
>({
id: args.id,
type: args.type,
rawConfig: args.rawConfig,
globalConfig: args.globalConfig,
args: args.args,
env: args.env,
initChildElement,
providers: undefined,
parsedConfig: undefined,
});
});

async function initChildElement(id: string) {
const childConfig = childConfigs.find(
childConfig => childConfig.id === id,
const { element, merged } = await getElementProviders(
elementContext,
args.ancestorProviders,
args.owner,
);

// Check whether an element with the given ID exists in the config.
if (!childConfig) {
return null;
}
setElementContext('providers', merged);

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,
const parsedConfig = getParsedElementConfig(
elementContext as PickPartial<ElementContext, 'parsedConfig'>,
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);

runWithOwner(args.owner, () => {
createEffect(() => {
if (parsedConfig.styles) {
styleBuilder.setElementStyles(
parsedConfig.id,
parsedConfig.styles,
);
}
});
});
}

return elementContext as ElementContext;
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 messageDialog((err as Error)?.message ?? 'Unknown reason.', {
title: 'Failed to initialize element!',
type: 'error',
});
}

throw err;
}
}
151 changes: 86 additions & 65 deletions packages/client-api/src/init-window.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { createEffect, getOwner, runWithOwner } from 'solid-js';
import { getCurrent as getCurrentWindow } from '@tauri-apps/api/window';
import { message as messageDialog } from '@tauri-apps/plugin-dialog';

import {
GlobalConfigSchema,
type UserConfig,
getUserConfig,
getStyleBuilder,
parseWithSchema,
} from './user-config';
import {
getOpenWindowArgs,
Expand All @@ -17,6 +19,9 @@ import {
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);
Expand All @@ -32,74 +37,90 @@ export function initWindow(callback: (context: WindowContext) => void) {
* * Building CSS and appending it to `<head>`
*/
export async function initWindowAsync(): Promise<WindowContext> {
// TODO: Create new root if owner is null.
const owner = getOwner()!;
const config = await getUserConfig();
const styleBuilder = getStyleBuilder(owner);

const openArgs =
window.__ZEBAR_OPEN_ARGS ??
(await getOpenWindowArgs(await getCurrentWindow().label));

const windowConfig = (config as UserConfig)[
`window/${openArgs.windowId}` as const
];

if (!windowConfig) {
throw new Error(
`Window '${openArgs.windowId}' doesn't exist in config. Is there a` +
`property for 'window/${openArgs.windowId}'?`,
try {
// TODO: Create new root if owner is null.
const owner = getOwner()!;
const config = await getUserConfig();
const styleBuilder = getStyleBuilder(owner);

const openArgs =
window.__ZEBAR_OPEN_ARGS ??
(await getOpenWindowArgs(getCurrentWindow().label));

const windowConfig = (config as UserConfig)[
`window/${openArgs.windowId}` as const
];

if (!windowConfig) {
throw new Error(
`Window \`${openArgs.windowId}\` isn\'t defined in the config. ` +
`Is there a property for \`window/${openArgs.windowId}\`?`,
);
}

const globalConfig = parseWithSchema(
GlobalConfigSchema.strip(),
(config as UserConfig)?.global ?? {},
);
}

const globalConfig = GlobalConfigSchema.strip().parse(
(config as UserConfig)?.global ?? {},
);

const windowContext = (await initElement({
id: openArgs.windowId,
type: ElementType.WINDOW,
rawConfig: windowConfig,
globalConfig,
args: openArgs.args,
env: openArgs.env,
ancestorProviders: [],
owner,
})) as WindowContext;

// Set global SCSS/CSS styles.
runWithOwner(owner, () => {
createEffect(() => {
if (windowContext.parsedConfig.global_styles) {
styleBuilder.setGlobalStyles(
windowContext.parsedConfig.global_styles,
);
}
const windowContext = (await initElement({
id: openArgs.windowId,
type: ElementType.WINDOW,
rawConfig: windowConfig,
globalConfig,
args: openArgs.args,
env: openArgs.env,
ancestorProviders: [],
owner,
})) as WindowContext;

// Set global SCSS/CSS styles.
runWithOwner(owner, () => {
createEffect(() => {
if (windowContext.parsedConfig.global_styles) {
styleBuilder.setGlobalStyles(
windowContext.parsedConfig.global_styles,
);
}
});
});
});

// 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<WindowStyles> = {
zOrder: windowContext.parsedConfig.z_order,
showInTaskbar: windowContext.parsedConfig.show_in_taskbar,
resizable: windowContext.parsedConfig.resizable,
};

const position: Partial<WindowPosition> = {
x: windowContext.parsedConfig.position_x,
y: windowContext.parsedConfig.position_y,
width: windowContext.parsedConfig.width,
height: windowContext.parsedConfig.height,
};

await setWindowStyles(styles);
await setWindowPosition(position);

// 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<WindowStyles> = {
zOrder: windowContext.parsedConfig.z_order,
showInTaskbar: windowContext.parsedConfig.show_in_taskbar,
resizable: windowContext.parsedConfig.resizable,
};

const position: Partial<WindowPosition> = {
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;
return windowContext;
} catch (err) {
logger.error('Failed to initialize window:', err);

await messageDialog((err as Error)?.message ?? 'Unknown reason.', {
title: 'Failed to initialize window!',
type: 'error',
});

// Error during window initialization is unrecoverable, so we close
// the window.
await getCurrentWindow().close();

throw err;
}
}
5 changes: 3 additions & 2 deletions packages/client-api/src/providers/get-element-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from 'solid-js';
import { createStore } from 'solid-js/store';

import { ProvidersConfigSchema } from '~/user-config';
import { ProvidersConfigSchema, parseWithSchema } from '~/user-config';
import type { ElementContext } from '~/element-context.model';
import { createProvider } from './create-provider';
import type { PickPartial } from '~/utils';
Expand Down Expand Up @@ -35,7 +35,8 @@ export async function getElementProviders(
* Get map of element providers.
*/
async function getElementProviders() {
const providerConfigs = ProvidersConfigSchema.parse(
const providerConfigs = parseWithSchema(
ProvidersConfigSchema,
(elementContext.rawConfig as Record<string, unknown>)?.providers ??
[],
);
Expand Down
Loading
Loading