Tools/psreadline.ps1
|
# Companion for psreadline — apply settings, theme, and register theme picker (fprl) # # Reads: settings in psreadline.json; $DFConfig.PSReadLineEditMode ('Emacs' default, # or 'Windows'); $DFConfig.PSReadLineTheme, then $DFConfig.Theme, then # 'catppuccin-mocha'. Theme files: $XDG_CONFIG_HOME\psreadline\themes\<name>.json, # else bundled Tools\psreadline\. # Changes: PSReadLine options for this session, including HistorySavePath, which # moves to $XDG_STATE_HOME\psreadline\history; earlier history in PowerShell's # default AppData file is not copied over. Binds Ctrl+p / Ctrl+n. # Do not change EditMode after Register-DFTool: Set-PSReadLineOption -EditMode # resets the Tab binding installed by the selected tab-completion role. # 1. Apply settings from tool JSON $_settings = $DFCurrentTool.settings if ($_settings) { $_settingsMap = @{ editMode = 'EditMode' predictionSource = 'PredictionSource' predictionViewStyle = 'PredictionViewStyle' bellStyle = 'BellStyle' historyNoDuplicates = 'HistoryNoDuplicates' historySaveStyle = 'HistorySaveStyle' historySearchCursorMovesToEnd = 'HistorySearchCursorMovesToEnd' maximumHistoryCount = 'MaximumHistoryCount' } $_optionArgs = @{} $_settings.PSObject.Properties | ForEach-Object { $_param = $_settingsMap[$_.Name] if ($_param) { $_optionArgs[$_param] = $_.Value } else { Write-Warning "DotForge: unknown PSReadLine setting '$($_.Name)' — skipping" } } $_editModeSetting = Get-DFConfig PSReadLineEditMode if ($null -ne $_editModeSetting) { if ($_editModeSetting -ieq 'Windows') { $_optionArgs['EditMode'] = 'Windows' } elseif ($_editModeSetting -ieq 'Emacs') { $_optionArgs['EditMode'] = 'Emacs' } else { Write-Warning "DotForge: invalid PSReadLineEditMode '$_editModeSetting' — retaining tool setting" } } if ($_optionArgs.Count -gt 0) { try { Set-PSReadLineOption @_optionArgs } catch [System.ArgumentException] { # PredictionSource/PredictionViewStyle throw when VT is unavailable # (e.g. redirected output in tests). Apply the remaining options individually. foreach ($_k in @($_optionArgs.Keys)) { $_single = [hashtable]::new() $_single[$_k] = $_optionArgs[$_k] try { Set-PSReadLineOption @_single } catch [System.ArgumentException] { Write-Warning "DotForge: PSReadLine option '$_k' not supported in this terminal — skipping" } } } } } # 2. Relocate the history file under $XDG_STATE_HOME (PowerShell otherwise # keeps it at its AppData default). xdg.dirs already created the directory. Set-PSReadLineOption -HistorySavePath (Join-Path (Get-DFXdgPath State) 'psreadline' 'history') # 3. History-search key handlers: Ctrl+p/Ctrl+n cycle history matching what's # already typed (vs. the default Up/Down, which cycle the whole history). Set-PSReadLineKeyHandler -Key Ctrl+p -Function HistorySearchBackward Set-PSReadLineKeyHandler -Key Ctrl+n -Function HistorySearchForward # 4. Register Invoke-DFApplyPSReadLineTheme (captures $_bundledDir via closure) $_bundledDir = Join-Path $PSScriptRoot 'psreadline' # Private helpers can't be called by name from a function:global: closure, but a # captured scriptblock keeps its module binding, so capture them here. $_resolveThemeFile = ${function:Resolve-DFThemeFile} $_xdgPath = ${function:Get-DFXdgPath} Set-Item -Path 'function:global:Invoke-DFApplyPSReadLineTheme' -Value ({ <# .SYNOPSIS Applies a PSReadLine syntax-color theme to this session. .DESCRIPTION Resolves -Name to a theme file: an absolute path is used as is, otherwise $XDG_CONFIG_HOME\psreadline\themes\<Name>.json, then DotForge's bundled Tools\psreadline\<Name>.json. The file holds a "colors" object mapping PSReadLine color names (Command, Parameter, String, Comment, …; see Set-PSReadLineOption -Colors) to #RRGGBB hex values, which are converted to 24-bit ANSI colors and applied with Set-PSReadLineOption -Colors. Invalid colors are skipped with a warning; an unknown theme warns and changes nothing. Defined by DotForge's psreadline companion, which calls it at startup with $DFConfig.PSReadLineTheme, then $DFConfig.Theme, then catppuccin-mocha. .PARAMETER Name A theme name (e.g. catppuccin-mocha) or the full path to a theme JSON file. .EXAMPLE Invoke-DFApplyPSReadLineTheme -Name catppuccin-mocha Applies the bundled Catppuccin Mocha colors to the command line. .EXAMPLE Invoke-DFApplyPSReadLineTheme -Name "$HOME\dotfiles\prl-theme.json" Applies a theme file you wrote, e.g. { "colors": { "Command": "#89b4fa", "String": "#a6e3a1" } }. .OUTPUTS None. Changes PSReadLine colors for the current session. .LINK https://github.com/simsrw73/DotForge/blob/main/docs/guide/tools.md #> [CmdletBinding()] param([Parameter(Mandatory)][string]$Name) $path = & $_resolveThemeFile -Tool 'psreadline' -Name $Name -BundledDir $_bundledDir if (-not $path) { Write-Warning "DotForge: PSReadLine theme '$Name' not found" return } $theme = Get-Content $path -Raw | ConvertFrom-Json $colors = @{} $theme.colors.PSObject.Properties | ForEach-Object { $hex = $_.Value if ($hex -match '^#[0-9A-Fa-f]{6}$') { $r = [Convert]::ToInt32($hex.Substring(1, 2), 16) $g = [Convert]::ToInt32($hex.Substring(3, 2), 16) $b = [Convert]::ToInt32($hex.Substring(5, 2), 16) $colors[$_.Name] = "`e[38;2;${r};${g};${b}m" } else { Write-Warning "DotForge: invalid color '$hex' for token '$($_.Name)' — skipping" } } if ($colors.Count -gt 0) { # Expose applied colors for testing in non-VT environments where # Get-PSReadLineOption.Colors returns $null (redirected output). $global:DFPSReadLineColors = $colors Set-PSReadLineOption -Colors $colors } }.GetNewClosure()) # 5. Apply initial theme: per-tool PSReadLineTheme -> shared Theme -> 'catppuccin-mocha'. $_themeSetting = Get-DFConfiguredTheme -ToolKey 'PSReadLineTheme' -Default 'catppuccin-mocha' $_themeSetting = Resolve-DFThemeName -Name $_themeSetting -ThemeMap $DFCurrentTool.themeMap Invoke-DFApplyPSReadLineTheme -Name $_themeSetting # 6. Register theme picker Set-Item -Path 'function:global:Select-PSReadLineTheme' -Value ({ <# .SYNOPSIS Fuzzy-picks a PSReadLine color theme and applies it to this session. .DESCRIPTION Lists your themes in $XDG_CONFIG_HOME\psreadline\themes followed by DotForge's bundled ones (a user theme hides a bundled theme of the same name) in fzf. Enter applies the selection with Invoke-DFApplyPSReadLineTheme for the current session only, and prints how to keep it: set $DFConfig['PSReadLineTheme'] in your profile. Defined by DotForge's psreadline companion; requires fzf (or $Env:Picker). .EXAMPLE fprl Opens the theme picker; Enter applies the highlighted theme. .OUTPUTS None. Writes a confirmation line to the host. .LINK https://github.com/simsrw73/DotForge/blob/main/docs/guide/tools.md #> [CmdletBinding()] param() $themes = [System.Collections.Generic.List[string]]::new() $seen = [System.Collections.Generic.HashSet[string]]::new( [System.StringComparer]::OrdinalIgnoreCase) $userDir = Join-Path (& $_xdgPath Config) 'psreadline' 'themes' if (Test-Path $userDir) { Get-ChildItem $userDir -Filter '*.json' | Sort-Object Name | ForEach-Object { if ($seen.Add($_.BaseName)) { $themes.Add($_.BaseName) } } } if (Test-Path $_bundledDir) { Get-ChildItem $_bundledDir -Filter '*.json' | Sort-Object Name | ForEach-Object { if ($seen.Add($_.BaseName)) { $themes.Add($_.BaseName) } } } if ($themes.Count -eq 0) { Write-Warning 'DotForge: no PSReadLine themes found' return } Invoke-DFPicker ` -List { $themes }.GetNewClosure() ` -Header 'Select PSReadLine theme [Enter to apply for this session]' ` -Action { param($n) Invoke-DFApplyPSReadLineTheme -Name $n Write-Host "Theme applied: $n (to persist: set `$Global:DFConfig['PSReadLineTheme'] = '$n')" -ForegroundColor Green } }.GetNewClosure()) Set-Alias -Name fprl -Value Select-PSReadLineTheme -Scope Global -Force |