import { statusDisplay, type ConfigurationItem } from "@vrtmrz/livesync-commonlib/compat/common/types"; import type { SettingControl, SettingDefinitionControl } from "obsidian"; import type { AutoWireOption, OnUpdateResult } from "./SettingPane.ts"; import type { AllBooleanItemKey, AllNumericItemKey, AllStringItemKey, OnDialogSettings } from "./settingConstants.ts"; export type PersistedBooleanSettingKey = Exclude; export type PersistedStringSettingKey = Exclude; export type PersistedNumericSettingKey = Exclude; export type PersistedSettingKey = PersistedBooleanSettingKey | PersistedStringSettingKey | PersistedNumericSettingKey; type SettingSpecBase = { key: K; control: C; visible?: () => boolean; disabled?: () => boolean; aliases?: string[]; }; type ToggleSettingSpec = SettingSpecBase< PersistedBooleanSettingKey, { type: "toggle"; defaultValue?: boolean; } >; type NumberSettingSpec = SettingSpecBase< PersistedNumericSettingKey, { type: "number"; min?: number; max?: number; allowZero?: boolean; } >; type DropdownSettingSpec = SettingSpecBase< PersistedStringSettingKey, { type: "dropdown"; options: () => Record; } >; export type SettingSpec = ToggleSettingSpec | NumberSettingSpec | DropdownSettingSpec; /** An existing Advanced-page panel and the standard controls rendered within it. */ export type SettingSpecGroup = { heading: string; items: readonly SettingSpec[]; }; export type SettingSpecMessages = { valueShouldBeInRange: (range: { min?: number; max?: number }) => string; }; type LegacySettingOptions = Pick; export type LegacySettingBinding = | { type: "toggle"; key: PersistedBooleanSettingKey; options: LegacySettingOptions & Pick; } | { type: "number"; key: PersistedNumericSettingKey; options: LegacySettingOptions & { clampMin?: number; clampMax?: number; acceptZero?: boolean; }; } | { type: "dropdown"; key: PersistedStringSettingKey; options: LegacySettingOptions & { options: Record }; }; export interface LegacySettingSpecRenderer { autoWireToggle(key: AllBooleanItemKey, options?: AutoWireOption): unknown; autoWireNumeric( key: AllNumericItemKey, options: AutoWireOption & { clampMin?: number; clampMax?: number; acceptZero?: boolean } ): unknown; autoWireDropDown(key: AllStringItemKey, options: AutoWireOption & { options: Record }): unknown; } function isToggleSettingSpec(spec: SettingSpec): spec is ToggleSettingSpec { return spec.control.type === "toggle"; } function isNumberSettingSpec(spec: SettingSpec): spec is NumberSettingSpec { return spec.control.type === "number"; } function toLegacyUpdateOptions(spec: SettingSpec): LegacySettingOptions { if (!spec.visible && !spec.disabled) { return {}; } return { onUpdate: (): OnUpdateResult => ({ ...(spec.visible ? { visibility: spec.visible() } : {}), ...(spec.disabled ? { disabled: spec.disabled() } : {}), }), }; } /** Convert a shared specification to the arguments accepted by the current AutoWire renderer. */ export function toLegacySettingBinding(spec: SettingSpec): LegacySettingBinding { const updateOptions = toLegacyUpdateOptions(spec); if (isToggleSettingSpec(spec)) { return { type: "toggle", key: spec.key, options: { ...updateOptions, ...(spec.control.defaultValue === undefined ? {} : { defaultToggleValue: spec.control.defaultValue }), }, }; } if (isNumberSettingSpec(spec)) { return { type: "number", key: spec.key, options: { ...updateOptions, ...(spec.control.min === undefined ? {} : { clampMin: spec.control.min }), ...(spec.control.max === undefined ? {} : { clampMax: spec.control.max }), ...(spec.control.allowZero === undefined ? {} : { acceptZero: spec.control.allowZero }), }, }; } return { type: "dropdown", key: spec.key, options: { ...updateOptions, options: spec.control.options(), }, }; } /** Render one shared specification without moving value or persistence ownership out of the settings tab. */ export function renderLegacySettingSpec(renderer: LegacySettingSpecRenderer, spec: SettingSpec): void { const binding = toLegacySettingBinding(spec); switch (binding.type) { case "toggle": renderer.autoWireToggle(binding.key, binding.options); return; case "number": renderer.autoWireNumeric(binding.key, binding.options); return; case "dropdown": renderer.autoWireDropDown(binding.key, binding.options); } } function numberIsOutOfRange(value: number, control: NumberSettingSpec["control"]) { if (!Number.isFinite(value)) { return true; } if (control.max !== undefined && value > control.max) { return true; } if (control.allowZero && value === 0) { return false; } return control.min !== undefined && value < control.min; } /** Check a value at the settings-tab persistence boundary against its shared control specification. */ export function isValidSettingSpecValue(spec: SettingSpec, value: unknown): boolean { switch (spec.control.type) { case "toggle": return typeof value === "boolean"; case "number": return typeof value === "number" && !numberIsOutOfRange(value, spec.control); case "dropdown": return typeof value === "string" && Object.prototype.hasOwnProperty.call(spec.control.options(), value); } } /** * Convert one shared specification to a declarative Obsidian control. * * This function returns a plain object and only refers to Obsidian through erased types, so Stage B does not load or * activate the 1.13 runtime API. Translated metadata and validation messages are explicit inputs to keep conversion * side-effect free. */ export function toObsidianSettingDefinition( spec: SettingSpec, metadata: Pick, messages: SettingSpecMessages ): SettingDefinitionControl { let control: SettingControl; switch (spec.control.type) { case "toggle": control = { type: "toggle", key: spec.key, ...(spec.control.defaultValue === undefined ? {} : { defaultValue: spec.control.defaultValue }), ...(spec.disabled ? { disabled: spec.disabled } : {}), }; break; case "number": { const numericControl = spec.control; const hasRange = numericControl.min !== undefined || numericControl.max !== undefined; control = { type: "number", key: spec.key, ...(numericControl.min === undefined ? {} : { min: numericControl.allowZero ? Math.min(0, numericControl.min) : numericControl.min }), ...(numericControl.max === undefined ? {} : { max: numericControl.max }), ...(metadata.placeHolder ? { placeholder: metadata.placeHolder } : {}), ...(hasRange ? { validate: (value: number) => numberIsOutOfRange(value, numericControl) ? messages.valueShouldBeInRange({ min: numericControl.min, max: numericControl.max, }) : undefined, } : {}), ...(spec.disabled ? { disabled: spec.disabled } : {}), }; break; } case "dropdown": control = { type: "dropdown", key: spec.key, options: spec.control.options(), ...(spec.disabled ? { disabled: spec.disabled } : {}), }; break; } return { name: `${metadata.name}${statusDisplay(metadata.status)}`, ...(metadata.desc ? { desc: metadata.desc } : {}), ...(spec.aliases ? { aliases: spec.aliases } : {}), ...(spec.visible ? { visible: spec.visible } : {}), control, }; }