dconf.psm1



#region private

function Get-DconfPath
{
    <#
        .DESCRIPTION
        Get all dconf paths that exist in the system.
    #>


    [CmdletBinding()]
    param
    (
        [switch]$Flush
    )

    if ($Flush -or -not $Script:DconfPaths)
    {
        $Dump = dconf dump /
        if (-not $?)
        {
            throw ($Dump | Out-String).Trim()
        }

        $KeyPaths = $Dump |
            Select-String '^\[(?<path>.*)\]$' |
            ForEach-Object Matches |
            ForEach-Object Groups |
            Where-Object Name -eq "path" |
            ForEach-Object Value

        # dconf dump omits paths that do not contain keys. For completions, we want those paths.
        # Assumption: dump output is ordered heirarchically
        $Last = ""
        $Script:DconfPaths = $KeyPaths | ForEach-Object {
            $Current = $_
            while ($Last -and $Current -notmatch "^$Last/.*")
            {
                $Last = $Last -replace '[^/]+?$' -replace '/$'
            }

            while ($Current -ne $Last)
            {
                $NextSegment = $Current -replace $Last -replace '^/' -replace '/.*'
                $Last = $Last, $NextSegment -join '/' -replace '^/'
                $Last
            }
        }
    }
    $Script:DconfPaths
}

function Resolve-DconfPath
{
    <#
        .DESCRIPTION
        Replaces relative paths with full paths in output from dconf dump.
    #>


    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory, Position = 0)]
        [Alias('Path')]
        [string]$BasePath,

        [Parameter(ValueFromPipeline, Mandatory, Position = 1)]
        [AllowEmptyString()]
        [string]$Text
    )

    process
    {
        $Lines = $Text -split '\r?\n'
        foreach ($Line in $Lines)
        {
            # This path is relative to the path passed to dconf dump
            if ($Line -match '^\[(?<Path>.+)\]\s*$')
            {
                $Path = $BasePath, $Matches.Path -join '/' -replace '/{2,}', '/' -replace '^/' -replace '/$'
                "[$Path]"
            }
            else
            {
                $Line
            }
        }
    }

    end {""}
}

function Set-Dconf
{
    <#
        .DESCRIPTION
        Imports dconf settings.

        .EXAMPLE
        dconf dump / > dump.txt
        Get-Content dump.txt | Set-Dconf
    #>


    [CmdletBinding()]
    param
    (
        [Parameter(Position = 0)]
        [string]$Path = '/',

        [Parameter(Mandatory, ValueFromPipeline, Position = 1)]
        [AllowEmptyString()]
        [string[]]$InputObject,

        [string[]]$Filter
    )

    end
    {
        $Path = $Path -replace '^/?', '/' -replace '(?<=[^/])$', '/'
        $_Path = $Path

        if ($Filter)
        {
            $Filter = @($Filter) -replace '^/?', '/' -replace '(?<=[^/])$', '/'
        }

        if ($MyInvocation.ExpectingInput)
        {
            $InputObject = $input
        }

        # Can't get past error: "Key file contains line [some_group] which is not a key-value pair, group, or comment"
        # So we use dconf write instead of dconf load
        $Lines = ($InputObject | Out-String).Trim() -split '\r?\n'
        foreach ($Line in $Lines)
        {
            if ([string]::IsNullOrWhiteSpace($Line) -or $Line.StartsWith('#'))
            {
                continue
            }
            elseif ($Line -match '^\[(?<Path>.+)\]\s*$')
            {
                $_Path = $(
                    $MatchedPath = $Matches.Path
                    if ($MatchedPath -eq '/')
                    {
                        $Path -replace '/$'
                    }
                    elseif ($MatchedPath -match '^/.')
                    {
                        $MatchedPath
                    }
                    else
                    {
                        $Path, $MatchedPath -join '/' -replace '/{2,}', '/'
                    }
                )

                $ShouldSkip = $Filter -and -not ($Filter | Where-Object {$_Path -ilike "$_*"})
                if ($ShouldSkip) {Write-Verbose "Skipping $_Path"}
                continue
            }

            if ($ShouldSkip) {continue}

            $Key, $Value = $Line -split '=', 2
            $FullKey = $_Path, $Key -join '/' -replace '/{2,}', '/'

            dconf write $FullKey "$Value"

            if (-not $?)
            {
                Write-Error "Failed to write '$Value' to '$FullKey'"
            }
        }
    }
}

#endregion private

#region public

function Export-Dconf
{
    <#
        .SYNOPSIS
        Exports dconf settings.

        .DESCRIPTION
        Exports dconf settings within a given dconf path to stdout or to file. Wraps `dconf dump`
        and converts dconf paths to absolute paths.

        .PARAMETER Path
        The dconf path to export.

        .PARAMETER OutFile
        The file to export to. By default, this command writes to stdout.

        .EXAMPLE
        Export-Dconf org/gnome/shell/extensions ./dconf.dump

        Exports settings under `/org/gnome/shell/extensions/` to `dconf.dump`.
    #>


    [CmdletBinding()]
    param
    (
        [Parameter(Position = 0)]
        [string[]]$Path = "/",

        [Parameter(Position = 1)]
        [string]$OutFile
    )

    $Content = @()

    foreach ($_Path in $Path)
    {
        $_Path = $_Path -replace '^/?', '/' -replace '(?<=[^/])$', '/'
        $Content += dconf dump $_Path | Resolve-DconfPath -Path $_Path
    }

    $Content = ($Content | Out-String).Trim()
    if ($OutFile)
    {
        $Content > $OutFile
    }
    elseif ($Content)
    {
        $Content
    }
}

function Import-Dconf
{
    <#
        .SYNOPSIS
        Imports dconf settings from file.

        .DESCRIPTION
        Imports dconf settings from file. Backs up all settings before importing.

        .PARAMETER File
        Path to file containing dconf settings. The file can be generated with `Export-Dconf` or
        with `dconf dump /`. Content should be key-value pairs, section headers in square brackets.

        Note that when using `dconf dump`, the section headers will be relative to the path passed
        to `dconf dump`. If this was not `/` (the root path), then the import will succeed, but
        incorrect paths and keys will be created. Export-Dconf prevents this by resolving paths to
        absolute paths.

        .PARAMETER Filter
        Limit the dconf paths to import. Only keys that are children of the dconf paths will be
        imported.

        .PARAMETER SkipBackup
        Do not back up dconf settings before import.

        .PARAMETER BackupPath
        Path to backup file. By default, this will be `/tmp/dconf.xxxxxxxxxxxxxxxxxx`.

        .EXAMPLE
        Import-Dconf -File ./dconf.dump

        Imports all settings from `dconf.dump`.

        .EXAMPLE
        Import-Dconf -File ./dconf.dump -Filter org/gnome/shell/extensions

        Imports settings under `/org/gnome/shell/extensions/` from `dconf.dump`.
    #>


    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory, Position = 0)]
        [string]$File,

        [string[]]$Filter,

        [switch]$SkipBackup,

        [string]$BackupPath = (Join-Path ([IO.Path]::GetTempPath()) "dconf.$([datetime]::UtcNow.Ticks)")
    )

    end
    {
        if (-not $SkipBackup)
        {
            Export-Dconf / -OutFile $BackupPath
            "Backed up dconf settings to $BackupPath" | Write-Verbose
        }

        $Content = Get-Content $File -ErrorAction Stop
        $Content | Set-Dconf -Filter $Filter
    }
}

#endregion public

$PathCompleter = {
    param ($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)

    $HasLeadingSlash = $wordToComplete -match '^/'
    $wordToComplete = $wordToComplete -replace '^/'
    $Paths = @(Get-DconfPath) -ilike "*$wordToComplete*"
    $DirectChildren = @($Paths) -imatch "^$wordToComplete([^/]*)$"
    $Paths = $DirectChildren, $Paths | Write-Output | Select-Object -Unique
    if ($HasLeadingSlash)
    {
        $Paths = @($Paths) -replace '^/?', '/'
    }
    $Paths
}
Register-ArgumentCompleter -CommandName Set-Dconf, Export-Dconf -ParameterName Path -ScriptBlock $PathCompleter
Register-ArgumentCompleter -CommandName Import-Dconf -ParameterName Filter -ScriptBlock $PathCompleter