dconf.psm1
#region private function ConvertTo-Dconf { <# .DESCRIPTION Formats KeyInfo objects as Dconf settings. #> [CmdletBinding()] param ( [Parameter(Mandatory, Position = 0, ValueFromPipeline)] [Dconf.KeyInfo[]]$InputObject ) end { if ($MyInvocation.ExpectingInput) { $InputObject = $input } $ByPath = $InputObject | Group-Object Path $ByPath | ForEach-Object { "[$($_.Name)]" $_.Group | ForEach-Object { "$($_.Key)=$($_.Value)" } "" } | Write-Output } } function ConvertTo-PSObject { <# .DESCRIPTION Creates KeyInfo objects representing Dconf settings. #> [CmdletBinding()] param ( [Parameter(Mandatory, Position = 0, ValueFromPipeline)] [AllowEmptyString()] [string[]]$InputObject ) end { if ($MyInvocation.ExpectingInput) { $InputObject = $input } $InputObject = $InputObject | Out-String | ForEach-Object Trim $InputObject -split "\n" | Where-Object {-not [string]::IsNullOrWhiteSpace($_)} | ForEach-Object { if ($_ -match '^\[(?<Path>.*)\]\s*$') { $Path = $Matches.Path } else { $Key, $Value = $_ -split '=', 2 [Dconf.KeyInfo]::new($Path, $Key, $Value) } } } } 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,}', '/' if ($Value -eq $Script:DCONF_RESET_SENTINEL) { dconf reset $FullKey } else { dconf write $FullKey "$Value" } if (-not $?) { Write-Error "Failed to write '$Value' to '$FullKey'" } } } } #endregion private #region public function Compare-Dconf { <# .SYNOPSIS Compares two sets of dconf settings. .DESCRIPTION Shows differences between two sets of dconf settings. By default, this command captures a snapshot of all settings, then waits for you to make changes before capturing a second snapshot. .PARAMETER ReferenceObject The "before" settings. If this argument is not supplied, the command will dump the current settings as a base for comparison. .PARAMETER DifferenceObject The "after" settings. If this argument is not supplied, the command will pause while you make dconf changes, then capture the new settings for comparison. .EXAMPLE Compare-Dconf Captured dconf snapshot Waiting to capture snapshot (press enter after making changes): [org/gnome/desktop/app-folders] folder-children=['Utilities', 'YaST', 'Pardus'] Captures the dconf changes made while the command was running. In this example, the user has modified the /org/gnome/desktop/app-folders/folder-children key. .EXAMPLE Export-Dconf > ./before <make dconf changes...> Export-Dconf > ./after Compare-Dconf (Get-Content ./before) (Get-Content ./after) [org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom0] binding='<Super>k' command='kitty' name='Kitty' In this example, the user set a keybinding for the kitty terminal between capturing the "before" and "after" snapshots. #> [CmdletBinding()] param ( [Parameter(Position = 0)] [string[]]$ReferenceObject, [Parameter(Position = 1)] [string[]]$DifferenceObject ) $Ref = if ($null -eq $ReferenceObject) { Export-Dconf | ConvertTo-PSObject Write-Host "Captured dconf snapshot" } else { $ReferenceObject | ConvertTo-PSObject } $Diff = if ($null -eq $DifferenceObject) { $null = Read-Host "Waiting to capture snapshot (press enter after making changes)" Export-Dconf | ConvertTo-PSObject } else { $DifferenceObject | ConvertTo-PSObject } $DiffsByPath = Compare-Object $Ref $Diff -Property FullName, Value | Group-Object FullName $Changed = $DiffsByPath | ForEach-Object { if ($_.Count -gt 1) { $_.Group | Where-Object SideIndicator -eq '=>' } elseif ($_.Group[0].SideIndicator -eq '=>') { $_.Group[0] } else { $Removed = $_.Group[0] $Removed.Value = $Script:DCONF_RESET_SENTINEL $Removed } } | Write-Output | ForEach-Object { $Path, $Key = $_.FullName -split '/(?=[^/]+/?$)', 2 [Dconf.KeyInfo]::new($Path, $Key, $_.Value) } $Changed | ConvertTo-Dconf } 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 $Script:DCONF_RESET_SENTINEL = "<default>" $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 |