src/UI/50-Files.ps1
|
# 50-Files.ps1 — config-file manager: list files, toggle Enabled, apply to # session, create (from scenario templates, #29), move, Includes wiring. function script:Build-PSMMFilesView { param( [Parameter(Mandatory)] $State, [Parameter(Mandatory)] $Metas, [string]$StatusMarkup ) $n = $Metas.Count $win = Get-PSMMWinSize $vp = Get-PSMMViewport -State $State -Count $n -Rows ($win.Height - 12) $T = [Spectre.Console.Table]::new() $T.Border = [Spectre.Console.TableBorder]::Rounded foreach ($h in ' ', 'File', 'Kind', 'On', 'RW', 'Mods', '!') { [void][Spectre.Console.TableExtensions]::AddColumn($T, $h) } for ($i = $vp.First; $i -le $vp.Last; $i++) { $m = $Metas[$i] $leaf = if ($m.Kind -eq 'inline') { 'profile inline' } else { Split-Path $m.Path -Leaf } $nm = ConvertTo-PSMMSafe $leaf if ($i -eq $State.Cursor) { $nm = "[$script:PSMM_ColAccent]$nm[/]" } $on = if ($m.Enabled) { '[green3]on[/]' } else { '[indianred1]off[/]' } $rw = if ($m.Writable) { 'rw' } else { 'ro' } $fl = if ($m.IncludesIgnored) { '[orange1]![/]' } else { ' ' } [void][Spectre.Console.TableExtensions]::AddRow($T, [string[]]@( $(if ($i -eq $State.Cursor) { "[$script:PSMM_ColAccent]>[/]" } else { ' ' }), $nm, $m.Kind, $on, $rw, "$($m.ModuleCount)", $fl)) } $pos = Get-PSMMPositionMarkup -State $State -Count $n -Viewport $vp $items = [System.Collections.Generic.List[Spectre.Console.Rendering.IRenderable]]::new() $items.Add([Spectre.Console.Markup]::new("[$script:PSMM_ColAccent]Config files[/]$pos [$script:PSMM_ColMute]- full path of current row below[/]")) $items.Add($T) if ($n) { $cur = $Metas[$State.Cursor] $items.Add([Spectre.Console.Markup]::new("[$script:PSMM_ColMute]$(ConvertTo-PSMMSafe $cur.Path)[/]")) if ($cur.IncludesIgnored) { $items.Add([Spectre.Console.Markup]::new('[orange1]! this file has an Includes section that is being ignored (main config only)[/]')) } } $items.Add([Spectre.Console.Markup]::new((Get-PSMMHint -Pairs @('up/dn=move', 'space=enable/disable+save', 'a=apply to session', '?=help', 'esc=back', 'Ctrl+Q=quit')))) $items.Add([Spectre.Console.Markup]::new((Get-PSMMHint -Pairs @('n=new config (templates)', 'm=move file', 'c=conflicts')))) foreach ($w in @(Get-PSMMWarning | Select-Object -First 4)) { $items.Add([Spectre.Console.Markup]::new("[orange1]$(ConvertTo-PSMMSafe $w)[/]")) } if ($StatusMarkup) { $items.Add([Spectre.Console.Markup]::new($StatusMarkup)) } [Spectre.Console.Rows]::new($items) } # Config file manager loop. function script:Show-PSMMFiles { $ui = $script:PSMM_UI $st = New-PSMMListState $st.Status = '' while ($true) { if ($ui.HardQuit) { return } $cmd = @{ Name = $null } Clear-PSMMScreen Invoke-PSMMLive -Body { param($ctx) while ($true) { if ($script:PSMM_UI.HardQuit) { return } $metas = @((Get-PSMMFileMeta).Values) $ctx.UpdateTarget((Build-PSMMFilesView -State $st -Metas $metas -StatusMarkup $st.Status)) $ctx.Refresh() $k = Read-PSMMKeyResize if ($null -eq $k) { continue } if (Test-PSMMHardQuitKey $k) { $script:PSMM_UI.HardQuit = $true; return } $st.Status = '' if (Invoke-PSMMListNav -State $st -KeyInfo $k -Count $metas.Count) { continue } switch ($k.Key) { ([ConsoleKey]::Spacebar) { $m = $metas[$st.Cursor] if ($m.Kind -eq 'inline' -or -not $m.Writable) { $st.Status = '[indianred1]read-only - cannot toggle[/]' } else { $m.Enabled = -not $m.Enabled Save-PSMMFile -Path $m.Path -Entries (Get-PSMMAllEntries) $script:PSMM_UI.Dirty = $true $st.Status = "[green3]saved ($(if ($m.Enabled) { 'enabled' } else { 'disabled' })) - 'a' applies load/unload changes[/]" } continue } ([ConsoleKey]::A) { $cmd.Name = 'apply'; return } ([ConsoleKey]::N) { $cmd.Name = 'new'; return } ([ConsoleKey]::M) { $cmd.Name = 'move'; return } ([ConsoleKey]::C) { $cmd.Name = 'conflicts'; return } ([ConsoleKey]::Escape) { return } default { if ($k.KeyChar -eq '?') { $cmd.Name = 'help'; return } } } } } if ($ui.HardQuit) { return } # full-screen follow-ups (prompts cannot run inside the live display) if ($ui.Dirty) { Sync-PSMMUIEntries -FullScan } switch ($cmd.Name) { 'apply' { Invoke-PSMMApply } 'new' { New-PSMMConfigFile } 'move' { $metas = @((Get-PSMMFileMeta).Values); if ($metas.Count) { Move-PSMMConfigFile -Meta $metas[$st.Cursor] } } 'conflicts' { Show-PSMMConflicts } 'help' { Show-PSMMHelpScreen -Topic 'files' } default { return } } if ($ui.Dirty) { Sync-PSMMUIEntries -FullScan } } } # Apply config changes to the live session: import newly-active Load-mode # modules; unload loaded modules that are managed but no longer active. function script:Invoke-PSMMApply { $ui = $script:PSMM_UI Clear-PSMMScreen Write-PSMMLine "[$script:PSMM_ColAccent]Apply config changes[/]" $activeEntries = @(Get-PSMMEntry) $active = @{}; foreach ($e in $activeEntries) { if ($e.Name) { $active[$e.Name] = $e } } $managed = @{}; foreach ($e in (Get-PSMMAllEntries)) { if ($e.Name) { $managed[$e.Name] = $true } } $did = 0 foreach ($e in $activeEntries) { if ($e.Mode -eq 'Load' -and -not (Get-Module -Name $e.Name)) { Write-PSMMLine "[$script:PSMM_ColAccent]loading $(ConvertTo-PSMMSafe $e.Name)...[/]" try { Import-PSMMModuleTimed -Entry $e; $did++ } catch { Write-PSMMLine "[indianred1] $(ConvertTo-PSMMSafe $_.Exception.Message)[/]" } } } foreach ($m in @(Get-Module)) { if ($managed.ContainsKey($m.Name) -and -not $active.ContainsKey($m.Name)) { Write-PSMMLine "[$script:PSMM_ColAccent]unloading $(ConvertTo-PSMMSafe $m.Name) (no longer active)...[/]" try { Remove-Module -Name $m.Name -Force -ErrorAction Stop; $did++ } catch { Write-PSMMLine "[indianred1] $(ConvertTo-PSMMSafe $_.Exception.Message)[/]" } } } $ui.Dirty = $true Write-PSMMLine "[green3]$did change(s) applied.[/]" $null = Wait-PSMMKey } # Scenario templates shipped with the module (#29). Key = menu label. function script:Get-PSMMConfigTemplate { [CmdletBinding()] param() $dir = Join-Path $script:PSMMRoot 'Configs' $templates = [ordered]@{ 'blank (example entry only)' = $null } foreach ($f in @(Get-ChildItem -LiteralPath $dir -Filter 'scenario-*.json' -ErrorAction SilentlyContinue | Sort-Object Name)) { $label = ($f.BaseName -replace '^scenario-', '') -replace '-', ' ' $templates[$label] = $f.FullName } $templates } function script:New-PSMMConfigFile { Clear-PSMMScreen Write-PSMMLine "[$script:PSMM_ColAccent]Create config file[/]" $main = Get-PSMMMainConfigPath $prof = Get-PSMMProfileConfigPath $opts = @() if (-not (Test-Path -LiteralPath $main)) { $opts += "main: $main" } if ($prof -and -not (Test-Path -LiteralPath $prof)) { $opts += "profile dir: $prof" } $opts += 'custom path...' $pick = Read-SpectreSelection -Message 'Where?' -Choices $opts -Color $script:PSMM_ColAccent $path = switch -Wildcard ($pick) { 'main:*' { $main } 'profile dir:*' { $prof } default { Read-SpectreText -Message 'Full path for the new .json' } } if ([string]::IsNullOrWhiteSpace($path)) { return } if (Test-Path -LiteralPath $path) { Write-PSMMLine '[indianred1]File already exists - not overwriting.[/]'; $null = Wait-PSMMKey; return } # scenario templates (#29) $templates = Get-PSMMConfigTemplate $tpl = Read-SpectreSelection -Message 'Start from' -Choices @($templates.Keys) -Color $script:PSMM_ColAccent $tplPath = $templates[$tpl] $dir = Split-Path -Parent $path if ($dir -and -not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null } $isMain = ($path -eq $main) if ($tplPath) { Copy-Item -LiteralPath $tplPath -Destination $path if ($isMain) { # main config carries an Includes array; add it if the template lacks one try { $obj = Get-Content -LiteralPath $path -Raw | ConvertFrom-Json if (-not $obj.PSObject.Properties['Includes']) { $obj | Add-Member -NotePropertyName Includes -NotePropertyValue @() ($obj | ConvertTo-Json -Depth 10) | Set-Content -LiteralPath $path -Encoding utf8 } } catch { } } Write-PSMMLine "[green3]Created $(ConvertTo-PSMMSafe $path) from the '$tpl' template.[/]" Write-PSMMLine "[grey66]All template modules are set to Mode=Ignore - flip the ones you want to Load/InstallOnly.[/]" } else { Get-PSMMExampleConfigJson -IsMain $isMain | Set-Content -LiteralPath $path -Encoding utf8 Write-PSMMLine "[green3]Created $(ConvertTo-PSMMSafe $path) (example content, sample module set to Ignore).[/]" } if (-not $isMain -and $path -ne $prof) { Write-PSMMLine "[orange1]Note: this path is not auto-discovered. Add it to the main config's Includes (or `$PSMM_JsonPath).[/]" $mainMeta = (Get-PSMMFileMeta)[$main] if ($mainMeta -and $mainMeta.Writable) { if (Read-SpectreConfirm -Message 'Add it to the main config Includes now?' -DefaultAnswer 'y') { $mainMeta.Includes = @($mainMeta.Includes) + $path Save-PSMMFile -Path $main -Entries (Get-PSMMAllEntries) Write-PSMMLine '[green3]Added to Includes.[/]' } } } $script:PSMM_UI.Dirty = $true $null = Wait-PSMMKey } function script:Move-PSMMConfigFile { param([Parameter(Mandatory)] $Meta) Clear-PSMMScreen Write-PSMMLine "[$script:PSMM_ColAccent]Move $(ConvertTo-PSMMSafe (Split-Path $Meta.Path -Leaf))[/]" if ($Meta.Kind -eq 'inline') { Write-PSMMLine '[indianred1]The inline profile block cannot be moved.[/]'; $null = Wait-PSMMKey; return } $dir = Read-SpectreText -Message 'Target folder' if ([string]::IsNullOrWhiteSpace($dir)) { return } if (-not (Test-Path -LiteralPath $dir -PathType Container)) { Write-PSMMLine '[indianred1]Folder not found.[/]'; $null = Wait-PSMMKey; return } $new = Join-Path $dir (Split-Path $Meta.Path -Leaf) if (Test-Path -LiteralPath $new) { Write-PSMMLine '[indianred1]A file with that name already exists there.[/]'; $null = Wait-PSMMKey; return } try { Move-Item -LiteralPath $Meta.Path -Destination $new -ErrorAction Stop } catch { Write-PSMMLine "[indianred1]$(ConvertTo-PSMMSafe $_.Exception.Message)[/]"; $null = Wait-PSMMKey; return } Write-PSMMLine "[green3]Moved to $(ConvertTo-PSMMSafe $new).[/]" # discovery follow-up: keep the file reachable $main = Get-PSMMMainConfigPath $mainMeta = (Get-PSMMFileMeta)[$main] $wasIncluded = $mainMeta -and (@($mainMeta.Includes) -contains $Meta.Path) if ($wasIncluded) { $mainMeta.Includes = @(@($mainMeta.Includes | Where-Object { $_ -ne $Meta.Path }) + $new) Save-PSMMFile -Path $main -Entries (Get-PSMMAllEntries) Write-PSMMLine '[green3]Main config Includes updated to the new path.[/]' } elseif ($Meta.Kind -in 'legacy', 'profile') { Write-PSMMLine '[orange1]The new location may not be auto-discovered.[/]' if ($mainMeta -and $mainMeta.Writable) { if (Read-SpectreConfirm -Message 'Add the new path to the main config Includes?' -DefaultAnswer 'y') { $mainMeta.Includes = @($mainMeta.Includes) + $new Save-PSMMFile -Path $main -Entries (Get-PSMMAllEntries) Write-PSMMLine '[green3]Added to Includes.[/]' } } } $script:PSMM_UI.Dirty = $true $null = Wait-PSMMKey } |