// Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import { ipcRenderer } from 'electron'; import { strictAssert } from './assert'; import * as Errors from '../types/errors'; import type { UnwrapPromise } from '../types/Util'; import type { IPCEventsValuesType, IPCEventsCallbacksType, } from './createIPCEvents'; import type { SystemTraySetting } from '../types/SystemTraySetting'; type SettingOptionsType = { getter?: boolean; setter?: boolean; }; export type SettingType = Readonly<{ getValue: () => Promise; setValue: (value: Value) => Promise; }>; export type ThemeType = 'light' | 'dark' | 'system'; export type EphemeralSettings = { spellCheck: boolean; systemTraySetting: SystemTraySetting; themeSetting: ThemeType; localeOverride: string | null; }; export type SettingsValuesType = IPCEventsValuesType & EphemeralSettings; type SettingGetterType = `get${Capitalize}`; type SettingSetterType = `set${Capitalize}`; type SettingUpdaterType = `update${Capitalize}`; function capitalize( name: Name ): Capitalize { const result = name.slice(0, 1).toUpperCase() + name.slice(1); return result as Capitalize; } function getSetterName( name: Key ): SettingSetterType { return `set${capitalize(name)}`; } function getGetterName( name: Key ): SettingGetterType { return `get${capitalize(name)}`; } function getUpdaterName( name: Key ): SettingUpdaterType { return `update${capitalize(name)}`; } export function createSetting< Name extends keyof SettingsValuesType, Value extends SettingsValuesType[Name] >(name: Name, overrideOptions: SettingOptionsType = {}): SettingType { const options = { getter: true, setter: true, ...overrideOptions, }; function getValue(): Promise { strictAssert(options.getter, `${name} has no getter`); return ipcRenderer.invoke(`settings:get:${name}`); } function setValue(value: Value): Promise { strictAssert(options.setter, `${name} has no setter`); return ipcRenderer.invoke(`settings:set:${name}`, value); } return { getValue, setValue, }; } type UnwrapReturn< // eslint-disable-next-line @typescript-eslint/no-explicit-any Callback extends (...args: Array) => unknown > = UnwrapPromise>; export function createCallback< Name extends keyof IPCEventsCallbacksType, Callback extends IPCEventsCallbacksType[Name] >( name: Name ): (...args: Parameters) => Promise> { return (...args: Parameters): Promise> => { return ipcRenderer.invoke(`settings:call:${name}`, args); }; } export function installSetting( name: keyof SettingsValuesType, { getter = true, setter = true }: { getter?: boolean; setter?: boolean } = {} ): void { const getterName = getGetterName(name); const setterName = getSetterName(name); if (getter) { ipcRenderer.on(`settings:get:${name}`, async (_event, { seq }) => { const getFn = window.Events[getterName]; if (!getFn) { ipcRenderer.send( `settings:get:${name}`, `installGetter: ${getterName} not found for event ${name}` ); return; } try { ipcRenderer.send('settings:response', seq, null, await getFn()); } catch (error) { ipcRenderer.send('settings:response', seq, Errors.toLogFormat(error)); } }); } if (setter) { ipcRenderer.on(`settings:set:${name}`, async (_event, { seq, value }) => { // Some settings do not have setters... // eslint-disable-next-line @typescript-eslint/no-explicit-any const setFn = (window.Events as any)[setterName] as ( value: unknown ) => Promise; if (!setFn) { ipcRenderer.send( 'settings:response', seq, `installSetter: ${setterName} not found for event ${name}` ); return; } try { await setFn(value); ipcRenderer.send('settings:response', seq, null); } catch (error) { ipcRenderer.send('settings:response', seq, Errors.toLogFormat(error)); } }); } } export function installEphemeralSetting(name: keyof EphemeralSettings): void { installSetting(name); const updaterName = getUpdaterName(name); ipcRenderer.on(`settings:update:${name}`, async (_event, value) => { const updateFn = window.Events[updaterName] as (value: unknown) => void; if (!updateFn) { return; } await updateFn(value); }); } export function installCallback( name: Name ): void { ipcRenderer.on(`settings:call:${name}`, async (_, { seq, args }) => { const hook = window.Events[name] as ( ...hookArgs: Array ) => Promise; try { ipcRenderer.send('settings:response', seq, null, await hook(...args)); } catch (error) { ipcRenderer.send('settings:response', seq, Errors.toLogFormat(error)); } }); }