2024-03-14 15:56:45 -04:00
|
|
|
import {
|
2024-04-02 10:29:34 -04:00
|
|
|
getInitialDefaultDir,
|
|
|
|
getSettingsFilePaths,
|
|
|
|
readSettingsFile,
|
|
|
|
} from '../tauriFS'
|
|
|
|
import { Setting, createSettings, settings } from 'lib/settings/initialSettings'
|
|
|
|
import { SaveSettingsPayload, SettingsLevel } from './settingsTypes'
|
|
|
|
import { isTauri } from 'lib/isTauri'
|
2024-04-09 08:04:36 -04:00
|
|
|
import { remove, writeTextFile, exists } from '@tauri-apps/plugin-fs'
|
2024-04-16 21:36:19 -07:00
|
|
|
import { initPromise, tomlParse, tomlStringify } from 'lang/wasm'
|
2024-03-14 15:56:45 -04:00
|
|
|
|
2024-04-02 10:29:34 -04:00
|
|
|
/**
|
|
|
|
* We expect the settings to be stored in a TOML file
|
|
|
|
* or TOML-formatted string in localStorage
|
|
|
|
* under a top-level [settings] key.
|
|
|
|
* @param path
|
|
|
|
* @returns
|
|
|
|
*/
|
|
|
|
function getSettingsFromStorage(path: string) {
|
|
|
|
return isTauri()
|
|
|
|
? readSettingsFile(path)
|
2024-04-16 21:36:19 -07:00
|
|
|
: (tomlParse(localStorage.getItem(path) ?? '')
|
2024-04-02 10:29:34 -04:00
|
|
|
.settings as Partial<SaveSettingsPayload>)
|
2024-03-14 15:56:45 -04:00
|
|
|
}
|
|
|
|
|
2024-04-02 10:29:34 -04:00
|
|
|
export async function loadAndValidateSettings(projectPath?: string) {
|
|
|
|
const settings = createSettings()
|
|
|
|
settings.app.projectDirectory.default = await getInitialDefaultDir()
|
|
|
|
// First, get the settings data at the user and project level
|
|
|
|
const settingsFilePaths = await getSettingsFilePaths(projectPath)
|
2024-03-14 15:56:45 -04:00
|
|
|
|
2024-04-02 10:29:34 -04:00
|
|
|
// Load the settings from the files
|
|
|
|
if (settingsFilePaths.user) {
|
2024-04-16 21:36:19 -07:00
|
|
|
await initPromise
|
2024-04-02 10:29:34 -04:00
|
|
|
const userSettings = await getSettingsFromStorage(settingsFilePaths.user)
|
|
|
|
if (userSettings) {
|
|
|
|
setSettingsAtLevel(settings, 'user', userSettings)
|
|
|
|
}
|
|
|
|
}
|
2024-03-14 15:56:45 -04:00
|
|
|
|
2024-04-02 10:29:34 -04:00
|
|
|
// Load the project settings if they exist
|
|
|
|
if (settingsFilePaths.project) {
|
|
|
|
const projectSettings = await getSettingsFromStorage(
|
|
|
|
settingsFilePaths.project
|
|
|
|
)
|
|
|
|
if (projectSettings) {
|
|
|
|
setSettingsAtLevel(settings, 'project', projectSettings)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return the settings object
|
|
|
|
return settings
|
2024-03-14 15:56:45 -04:00
|
|
|
}
|
|
|
|
|
2024-04-02 10:29:34 -04:00
|
|
|
export async function saveSettings(
|
|
|
|
allSettings: typeof settings,
|
|
|
|
projectPath?: string
|
|
|
|
) {
|
|
|
|
const settingsFilePaths = await getSettingsFilePaths(projectPath)
|
|
|
|
|
|
|
|
if (settingsFilePaths.user) {
|
|
|
|
const changedSettings = getChangedSettingsAtLevel(allSettings, 'user')
|
|
|
|
|
|
|
|
await writeOrClearPersistedSettings(settingsFilePaths.user, changedSettings)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (settingsFilePaths.project) {
|
|
|
|
const changedSettings = getChangedSettingsAtLevel(allSettings, 'project')
|
|
|
|
|
|
|
|
await writeOrClearPersistedSettings(
|
|
|
|
settingsFilePaths.project,
|
|
|
|
changedSettings
|
|
|
|
)
|
2024-03-14 15:56:45 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-02 10:29:34 -04:00
|
|
|
async function writeOrClearPersistedSettings(
|
|
|
|
settingsFilePath: string,
|
|
|
|
changedSettings: Partial<SaveSettingsPayload>
|
|
|
|
) {
|
2024-04-16 21:36:19 -07:00
|
|
|
await initPromise
|
2024-04-02 10:29:34 -04:00
|
|
|
if (changedSettings && Object.keys(changedSettings).length) {
|
|
|
|
if (isTauri()) {
|
|
|
|
await writeTextFile(
|
|
|
|
settingsFilePath,
|
2024-04-16 21:36:19 -07:00
|
|
|
tomlStringify({ settings: changedSettings })
|
2024-04-02 10:29:34 -04:00
|
|
|
)
|
2024-03-14 15:56:45 -04:00
|
|
|
}
|
2024-04-02 10:29:34 -04:00
|
|
|
localStorage.setItem(
|
|
|
|
settingsFilePath,
|
2024-04-16 21:36:19 -07:00
|
|
|
tomlStringify({ settings: changedSettings })
|
2024-04-02 10:29:34 -04:00
|
|
|
)
|
|
|
|
} else {
|
|
|
|
if (isTauri() && (await exists(settingsFilePath))) {
|
2024-04-09 08:04:36 -04:00
|
|
|
await remove(settingsFilePath)
|
2024-04-02 10:29:34 -04:00
|
|
|
}
|
|
|
|
localStorage.removeItem(settingsFilePath)
|
2024-03-14 15:56:45 -04:00
|
|
|
}
|
2024-04-02 10:29:34 -04:00
|
|
|
}
|
2024-03-14 15:56:45 -04:00
|
|
|
|
2024-04-02 10:29:34 -04:00
|
|
|
export function getChangedSettingsAtLevel(
|
|
|
|
allSettings: typeof settings,
|
|
|
|
level: SettingsLevel
|
|
|
|
): Partial<SaveSettingsPayload> {
|
|
|
|
const changedSettings = {} as Record<
|
|
|
|
keyof typeof settings,
|
|
|
|
Record<string, unknown>
|
|
|
|
>
|
|
|
|
Object.entries(allSettings).forEach(([category, settingsCategory]) => {
|
|
|
|
const categoryKey = category as keyof typeof settings
|
|
|
|
Object.entries(settingsCategory).forEach(
|
|
|
|
([setting, settingValue]: [string, Setting]) => {
|
|
|
|
// If setting is different its ancestors' non-undefined values,
|
|
|
|
// then it has been changed from the default
|
|
|
|
if (
|
|
|
|
settingValue[level] !== undefined &&
|
|
|
|
((level === 'project' &&
|
|
|
|
(settingValue.user !== undefined
|
|
|
|
? settingValue.project !== settingValue.user
|
|
|
|
: settingValue.project !== settingValue.default)) ||
|
|
|
|
(level === 'user' && settingValue.user !== settingValue.default))
|
|
|
|
) {
|
|
|
|
if (!changedSettings[categoryKey]) {
|
|
|
|
changedSettings[categoryKey] = {}
|
|
|
|
}
|
|
|
|
changedSettings[categoryKey][setting] = settingValue[level]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
})
|
2024-03-14 15:56:45 -04:00
|
|
|
|
2024-04-02 10:29:34 -04:00
|
|
|
return changedSettings
|
|
|
|
}
|
2024-03-14 15:56:45 -04:00
|
|
|
|
2024-04-02 10:29:34 -04:00
|
|
|
export function setSettingsAtLevel(
|
|
|
|
allSettings: typeof settings,
|
|
|
|
level: SettingsLevel,
|
|
|
|
newSettings: Partial<SaveSettingsPayload>
|
|
|
|
) {
|
|
|
|
Object.entries(newSettings).forEach(([category, settingsCategory]) => {
|
|
|
|
const categoryKey = category as keyof typeof settings
|
|
|
|
if (!allSettings[categoryKey]) return // ignore unrecognized categories
|
2024-04-05 00:30:11 -04:00
|
|
|
Object.entries(settingsCategory).forEach(([settingKey, settingValue]) => {
|
|
|
|
// TODO: How do you get a valid type for allSettings[categoryKey][settingKey]?
|
|
|
|
// it seems to always collapses to `never`, which is not correct
|
|
|
|
// @ts-ignore
|
|
|
|
if (!allSettings[categoryKey][settingKey]) return // ignore unrecognized settings
|
|
|
|
// @ts-ignore
|
|
|
|
allSettings[categoryKey][settingKey][level] = settingValue as unknown
|
|
|
|
})
|
2024-04-02 10:29:34 -04:00
|
|
|
})
|
|
|
|
|
|
|
|
return allSettings
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if the setting should be hidden
|
|
|
|
* based on its config, the current settings level,
|
|
|
|
* and the current platform.
|
|
|
|
*/
|
|
|
|
export function shouldHideSetting(
|
|
|
|
setting: Setting<unknown>,
|
|
|
|
settingsLevel: SettingsLevel
|
|
|
|
) {
|
|
|
|
return (
|
|
|
|
setting.hideOnLevel === settingsLevel ||
|
2024-04-05 00:30:11 -04:00
|
|
|
setting.hideOnPlatform === 'both' ||
|
2024-04-02 10:29:34 -04:00
|
|
|
(setting.hideOnPlatform && isTauri()
|
|
|
|
? setting.hideOnPlatform === 'desktop'
|
|
|
|
: setting.hideOnPlatform === 'web')
|
|
|
|
)
|
2024-03-14 15:56:45 -04:00
|
|
|
}
|
2024-04-05 00:30:11 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if the setting meets the requirements
|
|
|
|
* to appear in the settings modal in this context
|
|
|
|
* based on its config, the current settings level,
|
|
|
|
* and the current platform
|
|
|
|
*/
|
|
|
|
export function shouldShowSettingInput(
|
|
|
|
setting: Setting<unknown>,
|
|
|
|
settingsLevel: SettingsLevel
|
|
|
|
) {
|
|
|
|
return (
|
|
|
|
!shouldHideSetting(setting, settingsLevel) &&
|
|
|
|
(setting.Component ||
|
|
|
|
['string', 'boolean'].some((t) => typeof setting.default === t) ||
|
|
|
|
(setting.commandConfig?.inputType &&
|
|
|
|
['string', 'options', 'boolean'].some(
|
|
|
|
(t) => setting.commandConfig?.inputType === t
|
|
|
|
)))
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the appropriate input type to show given a
|
|
|
|
* command's config. Highly dependent on the filtering logic from
|
|
|
|
* shouldShowSettingInput being applied
|
|
|
|
*/
|
|
|
|
export function getSettingInputType(setting: Setting) {
|
|
|
|
if (setting.Component) return 'component'
|
|
|
|
if (setting.commandConfig)
|
|
|
|
return setting.commandConfig.inputType as 'string' | 'options' | 'boolean'
|
|
|
|
return typeof setting.default as 'string' | 'boolean'
|
|
|
|
}
|