// Shared by GNOME preferences and the daemon; no platform-specific imports. export function normalizeSettings(source, fields, { strict = false } = {}) { const config = source && typeof source === 'object' && !Array.isArray(source) ? source : {}; const result = {}; for (const field of fields) { let value = config[field.key]; if (value == null) { for (const alias of field.aliases || []) if (config[alias] != null) { value = config[alias]; break; } } if (value == null) value = field.default; if (field.type === 'boolean' && typeof value === 'string') { const flag = value.trim().toLowerCase(); if (['false', '0', 'off', 'no'].includes(flag)) value = false; else if (['true', '1', 'on', 'yes'].includes(flag)) value = true; } if (field.type === 'number' && typeof value === 'string' && value.trim()) value = Number(value); if (field.type === 'list' && typeof value === 'string') value = value.split(',').map(v => v.trim()).filter(Boolean); if (field.key === 'asrLanguage' && typeof value === 'string') value = value.toLowerCase().split(/[-_]/)[0]; if (['string', 'file', 'password'].includes(field.type) && typeof value === 'string') value = value.trim(); if (field.key === 'assistantName' && typeof value === 'string') { value = value.replace(/[\r\n\t]/g, ' ').replace(/\s+/g, ' ').slice(0, field.maxLength || 32); if (!value || /[<>&]/.test(value)) value = field.default; } if (field.type === 'text' && typeof value === 'string') { value = value.replace(/\0/g, '').replace(/\s+$/g, '').slice(0, field.maxLength || 4000); } const valid = field.type === 'boolean' ? typeof value === 'boolean' : field.type === 'number' ? Number.isFinite(value) && value >= field.min && value <= field.max && (field.step < 1 || Number.isInteger(value)) : field.type === 'choice' ? field.options.some(option => option.value === value) : field.type === 'list' ? Array.isArray(value) && value.every(item => typeof item === 'string') : field.type === 'text' ? typeof value === 'string' && value.length <= (field.maxLength || 4000) : field.type === 'password' ? typeof value === 'string' && value.length <= (field.maxLength || 256) : typeof value === 'string' && value.length <= (field.maxLength || 4096); if (!valid && strict) throw new Error(`Invalid value for ${field.title}`); result[field.key] = valid ? value : field.default; } if (result.vadMinSpeechMs > result.vadMaxSpeechSeconds * 1000) throw new Error('Minimum speech must be shorter than the maximum recording'); return result; } export function mergeSettings(source, changes, fields) { const merged = { ...source, ...changes }; // Remove obsolete aliases only for settings actually changed by this window. for (const field of fields) if (Object.hasOwn(changes, field.key)) { for (const alias of field.aliases || []) delete merged[alias]; } return merged; } export function settingVisible(field, values) { return !field.when || Object.entries(field.when).every(([key, choices]) => choices.includes(values[key])); } export function publicSettings(values, fields) { const out = { ...(values || {}) }; for (const field of fields || []) { if (field.type === 'password') out[field.key] = out[field.key] ? 'set' : ''; } return out; }