/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { Settings, SettingScope, LoadedSettings } from '../config/settings.js'; import { SETTINGS_SCHEMA, SettingDefinition, SettingsSchema, } from '../config/settingsSchema.js'; // The schema is now nested, but many parts of the UI and logic work better // with a flattened structure and dot-notation keys. This section flattens the // schema into a map for easier lookups. function flattenSchema( schema: SettingsSchema, prefix = '', ): Record { let result: Record = {}; for (const key in schema) { const newKey = prefix ? `${prefix}.${key}` : key; const definition = schema[key]; result[newKey] = { ...definition, key: newKey }; if (definition.properties) { result = { ...result, ...flattenSchema(definition.properties, newKey) }; } } return result; } const FLATTENED_SCHEMA = flattenSchema(SETTINGS_SCHEMA); /** * Get all settings grouped by category */ export function getSettingsByCategory(): Record< string, Array > { const categories: Record< string, Array > = {}; Object.values(FLATTENED_SCHEMA).forEach((definition) => { const category = definition.category; if (!categories[category]) { categories[category] = []; } categories[category].push(definition); }); return categories; } /** * Get a setting definition by key */ export function getSettingDefinition( key: string, ): (SettingDefinition & { key: string }) | undefined { return FLATTENED_SCHEMA[key]; } /** * Check if a setting requires restart */ export function requiresRestart(key: string): boolean { return FLATTENED_SCHEMA[key]?.requiresRestart ?? false; } /** * Get the default value for a setting */ export function getDefaultValue(key: string): SettingDefinition['default'] { return FLATTENED_SCHEMA[key]?.default; } /** * Get all setting keys that require restart */ export function getRestartRequiredSettings(): string[] { return Object.values(FLATTENED_SCHEMA) .filter((definition) => definition.requiresRestart) .map((definition) => definition.key); } /** * Recursively gets a value from a nested object using a key path array. */ export function getNestedValue( obj: Record, path: string[], ): unknown { const [first, ...rest] = path; if (!first || !(first in obj)) { return undefined; } const value = obj[first]; if (rest.length === 0) { return value; } if (value && typeof value === 'object' && value !== null) { return getNestedValue(value as Record, rest); } return undefined; } /** * Get the effective value for a setting, considering inheritance from higher scopes * Always returns a value (never undefined) - falls back to default if not set anywhere */ export function getEffectiveValue( key: string, settings: Settings, mergedSettings: Settings, ): SettingDefinition['default'] { const definition = getSettingDefinition(key); if (!definition) { return undefined; } const path = key.split('.'); // Check the current scope's settings first let value = getNestedValue(settings as Record, path); if (value !== undefined) { return value as SettingDefinition['default']; } // Check the merged settings for an inherited value value = getNestedValue(mergedSettings as Record, path); if (value !== undefined) { return value as SettingDefinition['default']; } // Return default value if no value is set anywhere return definition.default; } /** * Get all setting keys from the schema */ export function getAllSettingKeys(): string[] { return Object.keys(FLATTENED_SCHEMA); } /** * Get settings by type */ export function getSettingsByType( type: SettingDefinition['type'], ): Array { return Object.values(FLATTENED_SCHEMA).filter( (definition) => definition.type === type, ); } /** * Get settings that require restart */ export function getSettingsRequiringRestart(): Array< SettingDefinition & { key: string; } > { return Object.values(FLATTENED_SCHEMA).filter( (definition) => definition.requiresRestart, ); } /** * Validate if a setting key exists in the schema */ export function isValidSettingKey(key: string): boolean { return key in FLATTENED_SCHEMA; } /** * Get the category for a setting */ export function getSettingCategory(key: string): string | undefined { return FLATTENED_SCHEMA[key]?.category; } /** * Check if a setting should be shown in the settings dialog */ export function shouldShowInDialog(key: string): boolean { return FLATTENED_SCHEMA[key]?.showInDialog ?? true; // Default to true for backward compatibility } /** * Get all settings that should be shown in the dialog, grouped by category */ export function getDialogSettingsByCategory(): Record< string, Array > { const categories: Record< string, Array > = {}; Object.values(FLATTENED_SCHEMA) .filter((definition) => definition.showInDialog !== false) .forEach((definition) => { const category = definition.category; if (!categories[category]) { categories[category] = []; } categories[category].push(definition); }); return categories; } /** * Get settings by type that should be shown in the dialog */ export function getDialogSettingsByType( type: SettingDefinition['type'], ): Array { return Object.values(FLATTENED_SCHEMA).filter( (definition) => definition.type === type && definition.showInDialog !== false, ); } /** * Get all setting keys that should be shown in the dialog */ export function getDialogSettingKeys(): string[] { return Object.values(FLATTENED_SCHEMA) .filter((definition) => definition.showInDialog !== false) .map((definition) => definition.key); } // ============================================================================ // BUSINESS LOGIC UTILITIES (Higher-level utilities for setting operations) // ============================================================================ /** * Get the current value for a setting in a specific scope * Always returns a value (never undefined) - falls back to default if not set anywhere */ export function getSettingValue( key: string, settings: Settings, mergedSettings: Settings, ): boolean { const definition = getSettingDefinition(key); if (!definition) { return false; // Default fallback for invalid settings } const value = getEffectiveValue(key, settings, mergedSettings); // Ensure we return a boolean value, converting from the more general type if (typeof value === 'boolean') { return value; } // Fall back to default value, ensuring it's a boolean const defaultValue = definition.default; if (typeof defaultValue === 'boolean') { return defaultValue; } return false; // Final fallback } /** * Check if a setting value is modified from its default */ export function isSettingModified(key: string, value: boolean): boolean { const defaultValue = getDefaultValue(key); // Handle type comparison properly if (typeof defaultValue === 'boolean') { return value !== defaultValue; } // If default is not a boolean, consider it modified if value is true return value === true; } /** * Check if a setting exists in the original settings file for a scope */ export function settingExistsInScope( key: string, scopeSettings: Settings, ): boolean { const path = key.split('.'); const value = getNestedValue(scopeSettings as Record, path); return value !== undefined; } /** * Recursively sets a value in a nested object using a key path array. */ function setNestedValue( obj: Record, path: string[], value: unknown, ): Record { const [first, ...rest] = path; if (!first) { return obj; } if (rest.length === 0) { obj[first] = value; return obj; } if (!obj[first] || typeof obj[first] !== 'object') { obj[first] = {}; } setNestedValue(obj[first] as Record, rest, value); return obj; } /** * Set a setting value in the pending settings */ export function setPendingSettingValue( key: string, value: boolean, pendingSettings: Settings, ): Settings { const path = key.split('.'); const newSettings = JSON.parse(JSON.stringify(pendingSettings)); setNestedValue(newSettings, path, value); return newSettings; } /** * Generic setter: Set a setting value (boolean, number, string, etc.) in the pending settings */ export function setPendingSettingValueAny( key: string, value: unknown, pendingSettings: Settings, ): Settings { const path = key.split('.'); const newSettings = structuredClone(pendingSettings); setNestedValue(newSettings, path, value); return newSettings; } /** * Check if any modified settings require a restart */ export function hasRestartRequiredSettings( modifiedSettings: Set, ): boolean { return Array.from(modifiedSettings).some((key) => requiresRestart(key)); } /** * Get the restart required settings from a set of modified settings */ export function getRestartRequiredFromModified( modifiedSettings: Set, ): string[] { return Array.from(modifiedSettings).filter((key) => requiresRestart(key)); } /** * Save modified settings to the appropriate scope */ export function saveModifiedSettings( modifiedSettings: Set, pendingSettings: Settings, loadedSettings: LoadedSettings, scope: SettingScope, ): void { modifiedSettings.forEach((settingKey) => { const path = settingKey.split('.'); const value = getNestedValue( pendingSettings as Record, path, ); if (value === undefined) { return; } const existsInOriginalFile = settingExistsInScope( settingKey, loadedSettings.forScope(scope).settings, ); const isDefaultValue = value === getDefaultValue(settingKey); if (existsInOriginalFile || !isDefaultValue) { // This is tricky because setValue only works on top-level keys. // We need to set the whole parent object. const [parentKey] = path; if (parentKey) { const newParentValue = setPendingSettingValueAny( settingKey, value, loadedSettings.forScope(scope).settings, )[parentKey as keyof Settings]; loadedSettings.setValue( scope, parentKey as keyof Settings, newParentValue, ); } } }); } /** * Get the display value for a setting, showing current scope value with default change indicator */ export function getDisplayValue( key: string, settings: Settings, _mergedSettings: Settings, modifiedSettings: Set, pendingSettings?: Settings, ): string { // Prioritize pending changes if user has modified this setting let value: boolean; if (pendingSettings && settingExistsInScope(key, pendingSettings)) { // Show the value from the pending (unsaved) edits when it exists value = getSettingValue(key, pendingSettings, {}); } else if (settingExistsInScope(key, settings)) { // Show the value defined at the current scope if present value = getSettingValue(key, settings, {}); } else { // Fall back to the schema default when the key is unset in this scope const defaultValue = getDefaultValue(key); value = typeof defaultValue === 'boolean' ? defaultValue : false; } const valueString = String(value); // Check if value is different from default OR if it's in modified settings OR if there are pending changes const defaultValue = getDefaultValue(key); const isChangedFromDefault = typeof defaultValue === 'boolean' ? value !== defaultValue : value === true; const isInModifiedSettings = modifiedSettings.has(key); // Mark as modified if setting exists in current scope OR is in modified settings if (settingExistsInScope(key, settings) || isInModifiedSettings) { return `${valueString}*`; // * indicates setting is set in current scope } if (isChangedFromDefault || isInModifiedSettings) { return `${valueString}*`; // * indicates changed from default value } return valueString; } /** * Check if a setting doesn't exist in current scope (should be greyed out) */ export function isDefaultValue(key: string, settings: Settings): boolean { return !settingExistsInScope(key, settings); } /** * Check if a setting value is inherited (not set at current scope) */ export function isValueInherited( key: string, settings: Settings, _mergedSettings: Settings, ): boolean { return !settingExistsInScope(key, settings); } /** * Get the effective value for display, considering inheritance * Always returns a boolean value (never undefined) */ export function getEffectiveDisplayValue( key: string, settings: Settings, mergedSettings: Settings, ): boolean { return getSettingValue(key, settings, mergedSettings); }