mirror of
https://github.com/tw93/Mole.git
synced 2026-02-04 18:34:46 +00:00
450 lines
15 KiB
PowerShell
450 lines
15 KiB
PowerShell
# Mole - UI Module
|
|
# Provides interactive UI components (menus, confirmations, etc.)
|
|
|
|
#Requires -Version 5.1
|
|
Set-StrictMode -Version Latest
|
|
|
|
# Prevent multiple sourcing
|
|
if ((Get-Variable -Name 'MOLE_UI_LOADED' -Scope Script -ErrorAction SilentlyContinue) -and $script:MOLE_UI_LOADED) { return }
|
|
$script:MOLE_UI_LOADED = $true
|
|
|
|
# Import dependencies
|
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
. "$scriptDir\base.ps1"
|
|
. "$scriptDir\log.ps1"
|
|
|
|
# ============================================================================
|
|
# Terminal Utilities
|
|
# ============================================================================
|
|
|
|
function Get-TerminalSize {
|
|
<#
|
|
.SYNOPSIS
|
|
Get terminal width and height
|
|
#>
|
|
try {
|
|
return @{
|
|
Width = $Host.UI.RawUI.WindowSize.Width
|
|
Height = $Host.UI.RawUI.WindowSize.Height
|
|
}
|
|
}
|
|
catch {
|
|
return @{ Width = 80; Height = 24 }
|
|
}
|
|
}
|
|
|
|
function Clear-Line {
|
|
<#
|
|
.SYNOPSIS
|
|
Clear the current line
|
|
#>
|
|
$width = (Get-TerminalSize).Width
|
|
Write-Host -NoNewline ("`r" + (" " * ($width - 1)) + "`r")
|
|
}
|
|
|
|
function Move-CursorUp {
|
|
<#
|
|
.SYNOPSIS
|
|
Move cursor up N lines
|
|
#>
|
|
param([int]$Lines = 1)
|
|
Write-Host -NoNewline "$([char]27)[$Lines`A"
|
|
}
|
|
|
|
function Move-CursorDown {
|
|
<#
|
|
.SYNOPSIS
|
|
Move cursor down N lines
|
|
#>
|
|
param([int]$Lines = 1)
|
|
Write-Host -NoNewline "$([char]27)[$Lines`B"
|
|
}
|
|
|
|
# ============================================================================
|
|
# Confirmation Dialogs
|
|
# ============================================================================
|
|
|
|
function Read-Confirmation {
|
|
<#
|
|
.SYNOPSIS
|
|
Ask for yes/no confirmation
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string]$Prompt,
|
|
|
|
[bool]$Default = $false
|
|
)
|
|
|
|
$cyan = $script:Colors.Cyan
|
|
$nc = $script:Colors.NC
|
|
$hint = if ($Default) { "[Y/n]" } else { "[y/N]" }
|
|
|
|
Write-Host -NoNewline " ${cyan}$($script:Icons.Confirm)${nc} $Prompt $hint "
|
|
|
|
$response = Read-Host
|
|
|
|
if ([string]::IsNullOrWhiteSpace($response)) {
|
|
return $Default
|
|
}
|
|
|
|
return $response -match '^[Yy]'
|
|
}
|
|
|
|
function Read-ConfirmationDestructive {
|
|
<#
|
|
.SYNOPSIS
|
|
Ask for confirmation on destructive operations (requires typing 'yes')
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string]$Prompt,
|
|
|
|
[string]$ConfirmText = "yes"
|
|
)
|
|
|
|
$red = $script:Colors.Red
|
|
$nc = $script:Colors.NC
|
|
|
|
Write-Host ""
|
|
Write-Host " ${red}$($script:Icons.Warning) WARNING: $Prompt${nc}"
|
|
Write-Host " Type '$ConfirmText' to confirm: " -NoNewline
|
|
|
|
$response = Read-Host
|
|
return $response -eq $ConfirmText
|
|
}
|
|
|
|
# ============================================================================
|
|
# Menu Components
|
|
# ============================================================================
|
|
|
|
function Show-Menu {
|
|
<#
|
|
.SYNOPSIS
|
|
Display an interactive menu and return selected option
|
|
.PARAMETER Title
|
|
Menu title
|
|
.PARAMETER Options
|
|
Array of menu options (hashtables with Name and optionally Description, Action)
|
|
.PARAMETER AllowBack
|
|
Show back/exit option
|
|
#>
|
|
param(
|
|
[string]$Title = "Menu",
|
|
|
|
[Parameter(Mandatory)]
|
|
[array]$Options,
|
|
|
|
[switch]$AllowBack
|
|
)
|
|
|
|
$selected = 0
|
|
$maxIndex = $Options.Count - 1
|
|
|
|
# Add back option if allowed
|
|
if ($AllowBack) {
|
|
$Options = $Options + @{ Name = "Back"; Description = "Return to previous menu" }
|
|
$maxIndex++
|
|
}
|
|
|
|
$purple = $script:Colors.PurpleBold
|
|
$cyan = $script:Colors.Cyan
|
|
$gray = $script:Colors.Gray
|
|
$nc = $script:Colors.NC
|
|
|
|
# Hide cursor
|
|
Write-Host -NoNewline "$([char]27)[?25l"
|
|
|
|
try {
|
|
while ($true) {
|
|
# Clear screen and show menu
|
|
Clear-Host
|
|
|
|
Write-Host ""
|
|
Write-Host " ${purple}$($script:Icons.Arrow) $Title${nc}"
|
|
Write-Host ""
|
|
|
|
for ($i = 0; $i -le $maxIndex; $i++) {
|
|
$option = $Options[$i]
|
|
$name = if ($option -is [hashtable]) { $option.Name } else { $option.ToString() }
|
|
$desc = if ($option -is [hashtable] -and $option.Description) { " - $($option.Description)" } else { "" }
|
|
|
|
if ($i -eq $selected) {
|
|
Write-Host " ${cyan}> $name${nc}${gray}$desc${nc}"
|
|
}
|
|
else {
|
|
Write-Host " $name${gray}$desc${nc}"
|
|
}
|
|
}
|
|
|
|
Write-Host ""
|
|
Write-Host " ${gray}Use arrows or j/k to navigate, Enter to select, q to quit${nc}"
|
|
|
|
# Read key - handle both VirtualKeyCode and escape sequences
|
|
$key = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
|
|
|
|
# Debug: uncomment to see key codes
|
|
# Write-Host "VKey: $($key.VirtualKeyCode), Char: $([int]$key.Character)"
|
|
|
|
# Handle escape sequences for arrow keys (some terminals send these)
|
|
$moved = $false
|
|
if ($key.VirtualKeyCode -eq 0 -or $key.Character -eq [char]27) {
|
|
# Escape sequence - read the next characters
|
|
if ($Host.UI.RawUI.KeyAvailable) {
|
|
$key2 = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
|
|
if ($key2.Character -eq '[' -and $Host.UI.RawUI.KeyAvailable) {
|
|
$key3 = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
|
|
switch ($key3.Character) {
|
|
'A' { # Up arrow escape sequence
|
|
$selected = if ($selected -gt 0) { $selected - 1 } else { $maxIndex }
|
|
$moved = $true
|
|
}
|
|
'B' { # Down arrow escape sequence
|
|
$selected = if ($selected -lt $maxIndex) { $selected + 1 } else { 0 }
|
|
$moved = $true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (-not $moved) {
|
|
switch ($key.VirtualKeyCode) {
|
|
38 { # Up arrow
|
|
$selected = if ($selected -gt 0) { $selected - 1 } else { $maxIndex }
|
|
}
|
|
40 { # Down arrow
|
|
$selected = if ($selected -lt $maxIndex) { $selected + 1 } else { 0 }
|
|
}
|
|
13 { # Enter
|
|
# Show cursor
|
|
Write-Host -NoNewline "$([char]27)[?25h"
|
|
|
|
if ($AllowBack -and $selected -eq $maxIndex) {
|
|
return $null # Back selected
|
|
}
|
|
return $Options[$selected]
|
|
}
|
|
default {
|
|
switch ($key.Character) {
|
|
'k' { $selected = if ($selected -gt 0) { $selected - 1 } else { $maxIndex } }
|
|
'j' { $selected = if ($selected -lt $maxIndex) { $selected + 1 } else { 0 } }
|
|
'q' {
|
|
Write-Host -NoNewline "$([char]27)[?25h"
|
|
return $null
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
finally {
|
|
# Ensure cursor is shown
|
|
Write-Host -NoNewline "$([char]27)[?25h"
|
|
}
|
|
}
|
|
|
|
function Show-SelectionList {
|
|
<#
|
|
.SYNOPSIS
|
|
Display a multi-select list
|
|
#>
|
|
param(
|
|
[string]$Title = "Select Items",
|
|
|
|
[Parameter(Mandatory)]
|
|
[array]$Items,
|
|
|
|
[switch]$MultiSelect
|
|
)
|
|
|
|
$cursor = 0
|
|
$selected = @{}
|
|
$maxIndex = $Items.Count - 1
|
|
|
|
$purple = $script:Colors.PurpleBold
|
|
$cyan = $script:Colors.Cyan
|
|
$green = $script:Colors.Green
|
|
$gray = $script:Colors.Gray
|
|
$nc = $script:Colors.NC
|
|
|
|
Write-Host -NoNewline "$([char]27)[?25l"
|
|
|
|
try {
|
|
while ($true) {
|
|
Clear-Host
|
|
|
|
Write-Host ""
|
|
Write-Host " ${purple}$($script:Icons.Arrow) $Title${nc}"
|
|
if ($MultiSelect) {
|
|
Write-Host " ${gray}Space to toggle, Enter to confirm${nc}"
|
|
}
|
|
Write-Host ""
|
|
|
|
for ($i = 0; $i -le $maxIndex; $i++) {
|
|
$item = $Items[$i]
|
|
$name = if ($item -is [hashtable]) { $item.Name } else { $item.ToString() }
|
|
$check = if ($selected[$i]) { "$($script:Icons.Success)" } else { "$($script:Icons.Empty)" }
|
|
|
|
if ($i -eq $cursor) {
|
|
Write-Host " ${cyan}> ${check} $name${nc}"
|
|
}
|
|
else {
|
|
$checkColor = if ($selected[$i]) { $green } else { $gray }
|
|
Write-Host " ${checkColor}${check}${nc} $name"
|
|
}
|
|
}
|
|
|
|
Write-Host ""
|
|
Write-Host " ${gray}j/k or arrows to navigate, space to select, Enter to confirm, q to cancel${nc}"
|
|
|
|
$key = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
|
|
|
|
# Handle escape sequences for arrow keys (some terminals send these)
|
|
$moved = $false
|
|
if ($key.VirtualKeyCode -eq 0 -or $key.Character -eq [char]27) {
|
|
# Escape sequence - read the next characters
|
|
if ($Host.UI.RawUI.KeyAvailable) {
|
|
$key2 = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
|
|
if ($key2.Character -eq '[' -and $Host.UI.RawUI.KeyAvailable) {
|
|
$key3 = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
|
|
switch ($key3.Character) {
|
|
'A' { # Up arrow escape sequence
|
|
$cursor = if ($cursor -gt 0) { $cursor - 1 } else { $maxIndex }
|
|
$moved = $true
|
|
}
|
|
'B' { # Down arrow escape sequence
|
|
$cursor = if ($cursor -lt $maxIndex) { $cursor + 1 } else { 0 }
|
|
$moved = $true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (-not $moved) {
|
|
switch ($key.VirtualKeyCode) {
|
|
38 { $cursor = if ($cursor -gt 0) { $cursor - 1 } else { $maxIndex } }
|
|
40 { $cursor = if ($cursor -lt $maxIndex) { $cursor + 1 } else { 0 } }
|
|
32 { # Space
|
|
if ($MultiSelect) {
|
|
$selected[$cursor] = -not $selected[$cursor]
|
|
}
|
|
else {
|
|
$selected = @{ $cursor = $true }
|
|
}
|
|
}
|
|
13 { # Enter
|
|
Write-Host -NoNewline "$([char]27)[?25h"
|
|
$result = @()
|
|
foreach ($selKey in $selected.Keys) {
|
|
if ($selected[$selKey]) {
|
|
$result += $Items[$selKey]
|
|
}
|
|
}
|
|
return $result
|
|
}
|
|
default {
|
|
switch ($key.Character) {
|
|
'k' { $cursor = if ($cursor -gt 0) { $cursor - 1 } else { $maxIndex } }
|
|
'j' { $cursor = if ($cursor -lt $maxIndex) { $cursor + 1 } else { 0 } }
|
|
' ' {
|
|
if ($MultiSelect) {
|
|
$selected[$cursor] = -not $selected[$cursor]
|
|
}
|
|
else {
|
|
$selected = @{ $cursor = $true }
|
|
}
|
|
}
|
|
'q' {
|
|
Write-Host -NoNewline "$([char]27)[?25h"
|
|
return @()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
finally {
|
|
Write-Host -NoNewline "$([char]27)[?25h"
|
|
}
|
|
}
|
|
|
|
# ============================================================================
|
|
# Banner / Header
|
|
# ============================================================================
|
|
|
|
function Show-Banner {
|
|
<#
|
|
.SYNOPSIS
|
|
Display the Mole ASCII banner
|
|
#>
|
|
$purple = $script:Colors.Purple
|
|
$cyan = $script:Colors.Cyan
|
|
$nc = $script:Colors.NC
|
|
|
|
Write-Host ""
|
|
Write-Host " ${purple}MOLE${nc}"
|
|
Write-Host " ${cyan}Windows System Maintenance${nc}"
|
|
Write-Host ""
|
|
}
|
|
|
|
function Show-Header {
|
|
<#
|
|
.SYNOPSIS
|
|
Display a section header
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string]$Title,
|
|
|
|
[string]$Subtitle = ""
|
|
)
|
|
|
|
$purple = $script:Colors.PurpleBold
|
|
$gray = $script:Colors.Gray
|
|
$nc = $script:Colors.NC
|
|
|
|
Write-Host ""
|
|
Write-Host " ${purple}$Title${nc}"
|
|
if ($Subtitle) {
|
|
Write-Host " ${gray}$Subtitle${nc}"
|
|
}
|
|
Write-Host ""
|
|
}
|
|
|
|
# ============================================================================
|
|
# Summary Display
|
|
# ============================================================================
|
|
|
|
function Show-Summary {
|
|
<#
|
|
.SYNOPSIS
|
|
Display cleanup summary
|
|
#>
|
|
param(
|
|
[long]$SizeBytes = 0,
|
|
[int]$ItemCount = 0,
|
|
[string]$Action = "Cleaned"
|
|
)
|
|
|
|
$green = $script:Colors.Green
|
|
$cyan = $script:Colors.Cyan
|
|
$nc = $script:Colors.NC
|
|
|
|
$sizeHuman = Format-ByteSize -Bytes $SizeBytes
|
|
|
|
Write-Host ""
|
|
Write-Host " $($green)━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━$($nc)"
|
|
Write-Host " $($green)$($script:Icons.Success)$($nc) $($Action): $($cyan)$($sizeHuman)$($nc) across $($cyan)$($ItemCount)$($nc) items"
|
|
Write-Host " $($green)━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━$($nc)"
|
|
Write-Host ""
|
|
}
|
|
|
|
# ============================================================================
|
|
# Exports (functions are available via dot-sourcing)
|
|
# ============================================================================
|
|
# Functions: Show-Menu, Show-Banner, Read-Confirmation, etc.
|