private/80-settings.ps1

# -------------------------------------------------------------------- settings --

function Get-UiSettingsFile {
    $ctx = Assert-UiBuilder
    $windows = ($PSVersionTable.PSEdition -eq 'Desktop') -or ((Get-Variable -Name IsWindows -ErrorAction SilentlyContinue) -and $IsWindows)
    if ($windows) {
        $base = if ($env:APPDATA) { $env:APPDATA } else { Join-Path $HOME 'AppData\Roaming' }
        $dir = Join-Path (Join-Path $base 'UI4ME') $ctx.AppId
    } else {
        $base = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { Join-Path $HOME '.config' }
        $dir = Join-Path (Join-Path $base 'ui4me') $ctx.AppId
    }
    return (Join-Path $dir 'settings.json')
}

function Get-UiSettings {
    $ctx = Assert-UiBuilder
    if ($null -ne $ctx.Settings) { return $ctx.Settings }
    $table = [ordered]@{}
    $file = Get-UiSettingsFile
    if (Test-Path -LiteralPath $file) {
        try {
            if ((Get-Item -LiteralPath $file).Length -gt 1MB) { throw 'settings file is larger than 1 MiB' }
            $parsed = [System.IO.File]::ReadAllText($file) | ConvertFrom-Json -ErrorAction Stop
            if ($parsed -isnot [System.Management.Automation.PSCustomObject]) { throw 'settings file must contain a JSON object' }
            foreach ($p in $parsed.PSObject.Properties) { $table[$p.Name] = $p.Value }
        } catch {
            Move-Item -LiteralPath $file -Destination ($file + '.corrupt') -Force -ErrorAction SilentlyContinue
            Write-UiError $_ "settings $file"
        }
    }
    $ctx.Settings = $table
    return $table
}

function Get-UI4MEUiSetting {
    <#
    .SYNOPSIS
    Reads a persistent per-user setting of this app (same settings.json as Python's app.settings).
    .DESCRIPTION
    Stored in %APPDATA%\UI4ME\<AppId>\settings.json (Windows) or ~/.config/ui4me/<AppId>/settings.json (Linux).
    Invoke-UI4MEApp -AppId sets the folder (default: derived from -Title).
    #>

    [CmdletBinding()]
    param([Parameter(Mandatory)][string]$Name, [AllowNull()][object]$Default)
    $table = Get-UiSettings
    if ($table.Contains($Name)) { return $table[$Name] }
    return $Default
}

function Set-UI4MEUiSetting {
    <# .SYNOPSIS Stores a persistent setting (JSON-compatible value) atomically. -Remove deletes it. #>
    [CmdletBinding(DefaultParameterSetName = 'Set')]
    param([Parameter(Mandatory)][string]$Name, [Parameter(ParameterSetName = 'Set', Mandatory)][AllowNull()][object]$Value,
          [Parameter(ParameterSetName = 'Remove', Mandatory)][switch]$Remove)
    if (-not $Name -or $Name.Length -gt 200) { throw 'Setting names must be 1-200 characters' }
    $table = Get-UiSettings
    if ($Remove) { if (-not $table.Contains($Name)) { return }; $table.Remove($Name) } else { $table[$Name] = $Value }
    $json = ConvertTo-Json -InputObject ([pscustomobject]$table) -Depth 10
    if ([System.Text.Encoding]::UTF8.GetByteCount($json) -gt 1MB) { throw 'Settings are limited to 1 MiB' }
    $file = Get-UiSettingsFile
    $dir = Split-Path -Parent $file
    if (-not (Test-Path -LiteralPath $dir)) { $null = New-Item -ItemType Directory -Path $dir -Force }
    $tmp = Join-Path $dir ('.settings-' + [guid]::NewGuid().ToString('N') + '.tmp')
    [System.IO.File]::WriteAllText($tmp, $json, (New-Object System.Text.UTF8Encoding($false)))
    if (Test-Path -LiteralPath $file) { [System.IO.File]::Replace($tmp, $file, [NullString]::Value) }  # [NullString]: $null would become ""
    else { [System.IO.File]::Move($tmp, $file) }
}