private/OTP-PlatformUI.ps1
# Platform-specific UI implementations class ConsoleUI { [System.Collections.Generic.List[object]]$Codes [bool]$Running = $false [int]$UpdateInterval = 30 [System.Timers.Timer]$Timer hidden [int]$MaxSeedWidth = 0 hidden [int]$MaxCodeWidth = 0 hidden [int]$MaxAlgoWidth = 0 hidden [int]$MaxHashWidth = 0 hidden [int]$LastUpdateTime = 0 hidden [object]$SyncRoot = [System.Object]::new() ConsoleUI() { $this.Codes = [System.Collections.Generic.List[object]]::new() $this.Timer = [System.Timers.Timer]::new() $this.Timer.Interval = 1000 # 1 second # Use synchronized timer event to avoid cross-thread issues $this.Timer.Add_Elapsed({ if (-not $this.Running) { return } # Synchronize timer updates to avoid multiple simultaneous updates [System.Threading.Monitor]::Enter($this.SyncRoot) try { $this.UpdateTimerDisplay() } finally { [System.Threading.Monitor]::Exit($this.SyncRoot) } }) } hidden [void]UpdateColumnWidths() { $this.MaxSeedWidth = 0 $this.MaxCodeWidth = 0 $this.MaxAlgoWidth = 0 $this.MaxHashWidth = 0 foreach ($code in $this.Codes) { $this.MaxSeedWidth = [Math]::Max($this.MaxSeedWidth, $code.Seed.Length) $this.MaxCodeWidth = [Math]::Max($this.MaxCodeWidth, $code.Code.Length) $this.MaxAlgoWidth = [Math]::Max($this.MaxAlgoWidth, $code.Algorithm.Length) $this.MaxHashWidth = [Math]::Max($this.MaxHashWidth, $code.HashAlgorithm.Length) } } hidden [int]GetRemainingSeconds() { # Calculate remaining seconds until next 30-second interval $unixTime = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() return 30 - ($unixTime % 30) } hidden [void]UpdateTimerDisplay() { try { # Check if we need to update based on time $currentTime = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() if (($currentTime - $this.LastUpdateTime) -lt 1) { return } $this.LastUpdateTime = $currentTime # Save current cursor position $currentTop = [System.Console]::CursorTop $currentLeft = [System.Console]::CursorLeft # Move to the last line [System.Console]::SetCursorPosition(0, [System.Console]::WindowTop + [System.Console]::WindowHeight - 1) # Get remaining seconds and check if we need to update codes $remainingSeconds = $this.GetRemainingSeconds() if ($remainingSeconds -eq 30) { $this.UpdateDisplay($true) } else { # Update timer display $timerLine = "Next update in: $remainingSeconds seconds" Write-Host $timerLine.PadRight([Console]::WindowWidth - 1) -NoNewline -ForegroundColor Green } # Restore cursor position [System.Console]::SetCursorPosition($currentLeft, $currentTop) } catch { # Ignore any console errors } } [void]ShowCodes() { $this.Running = $true $this.UpdateColumnWidths() $this.UpdateDisplay($true) $this.Timer.Start() try { while ($this.Running) { if ([Console]::KeyAvailable) { $key = [Console]::ReadKey($true) if ($key.Key -eq [ConsoleKey]::Q) { $this.Running = $false } elseif ($key.Key -eq [ConsoleKey]::R) { $this.UpdateDisplay($true) } } Start-Sleep -Milliseconds 100 } } finally { $this.Timer.Stop() } } [void]UpdateDisplay([bool]$force) { if (-not $this.Running) { return } Clear-Host Write-Host "One-Time Password Codes`n" -ForegroundColor Cyan Write-Host "Press 'Q' to quit, 'R' to refresh`n" -ForegroundColor Yellow # Header Write-Host "Tag".PadRight(20) -NoNewline -ForegroundColor DarkGray Write-Host "Code".PadRight($this.MaxCodeWidth + 2) -NoNewline -ForegroundColor DarkGray Write-Host "Algorithm".PadRight($this.MaxAlgoWidth + 2) -NoNewline -ForegroundColor DarkGray Write-Host "Hash".PadRight($this.MaxHashWidth + 2) -NoNewline -ForegroundColor DarkGray Write-Host "Seed" -ForegroundColor DarkGray Write-Host ("-" * ([Console]::WindowWidth - 1)) -ForegroundColor DarkGray foreach ($code in $this.Codes) { # Tag with fixed width $tag = if ($code.Tag) { "[$($code.Tag -join ', ')] " } else { "" } Write-Host $tag.PadRight(20) -NoNewline -ForegroundColor Blue # Code with fixed width Write-Host $code.Code.PadRight($this.MaxCodeWidth + 2) -NoNewline -ForegroundColor White # Algorithm and Hash with fixed width Write-Host $code.Algorithm.PadRight($this.MaxAlgoWidth + 2) -NoNewline -ForegroundColor Gray Write-Host $code.HashAlgorithm.PadRight($this.MaxHashWidth + 2) -NoNewline -ForegroundColor Gray # Show full seed Write-Host $code.Seed -ForegroundColor Gray } Write-Host "`nNext update in: $($this.GetRemainingSeconds()) seconds" -NoNewline -ForegroundColor Green } [void]AddCode([object]$code) { # Validate code object if (-not $code.Seed -or -not $code.Code -or -not $code.Algorithm) { throw [System.ArgumentException]::new("Invalid code object. Missing required properties.") } $this.Codes.Add($code) $this.UpdateColumnWidths() } [void]Dispose() { $this.Running = $false if ($this.Timer) { $this.Timer.Stop() $this.Timer.Dispose() } # Clear sensitive data foreach ($code in $this.Codes) { $code.Seed = $null $code.Code = $null } $this.Codes.Clear() } } function New-OTPUI { [CmdletBinding()] param( [Parameter()] [switch]$ForceConsole ) # If ForceConsole is specified, always use console UI if ($ForceConsole) { Write-Verbose "Using console UI as requested" return [ConsoleUI]::new() } # Check platform $isWindows = $PSVersionTable.PSVersion.Major -ge 6 -and $IsWindows -or $PSVersionTable.PSVersion.Major -lt 6 -and $true if ($isWindows) { $wpfUI = Initialize-OTPUI if ($wpfUI) { return $wpfUI } } # Fall back to console UI if WPF is not available or not on Windows return [ConsoleUI]::new() } |