Last active
December 31, 2025 01:11
-
-
Save superyngo/c77bbbf67759cc4213aac12ebe980225 to your computer and use it in GitHub Desktop.
Shell and PowerShell utility scripts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| .SYNOPSIS | |
| mini-nano.ps1 - A lightweight PowerShell-based text editor | |
| .DESCRIPTION | |
| A terminal-based text editor similar to nano, written entirely in PowerShell. | |
| Provides essential text editing features with support for multiple file types | |
| and automatic comment symbol detection. | |
| .FEATURES | |
| - Basic text editing with cursor navigation | |
| - Line numbers display (toggleable with Ctrl+L) | |
| - Selection support with Shift+Arrows | |
| - Copy/Cut/Paste operations (Ctrl+Alt+C/X/V) | |
| - Undo/Redo functionality (Ctrl+Z) | |
| - Auto comment/uncomment for current line (Ctrl+/) | |
| - Page navigation (Page Up/Down) | |
| - Home/End key support | |
| - Tab support (4 spaces) | |
| - Multi-line paste support | |
| - File auto-save | |
| .KEYBINDINGS | |
| F2 - Save file | |
| Ctrl+Q / ESC - Quit editor | |
| Ctrl+Z - Undo last action | |
| Ctrl+Y - Redo action | |
| Ctrl+A - Select all | |
| Ctrl+L - Toggle line numbers | |
| Ctrl+/ - Toggle comment current line | |
| Ctrl+Alt+X - Cut selection | |
| Ctrl+Alt+C - Copy selection | |
| Ctrl+Alt+V - Paste from clipboard | |
| Arrow Keys - Move cursor | |
| Shift+Arrow Keys - Extend selection | |
| Page Up/Down - Scroll page | |
| Home/End - Move to line start/end | |
| Tab - Insert 4 spaces | |
| Enter - New line | |
| Backspace - Delete character | |
| Delete - Delete next character | |
| .SUPPORTED_FILE_TYPES | |
| PowerShell (.ps1, .psm1) - # comment symbol | |
| Shell Script (.sh, .bash) - # comment symbol | |
| Python (.py) - # comment symbol | |
| Ruby (.rb) - # comment symbol | |
| YAML (.yaml, .yml) - # comment symbol | |
| Batch (.bat, .cmd) - REM comment symbol | |
| JavaScript (.js) - // comment symbol | |
| TypeScript (.ts) - // comment symbol | |
| C#/.NET (.cs) - // comment symbol | |
| Java (.java) - // comment symbol | |
| C/C++ (.c, .cpp) - // comment symbol | |
| Go (.go) - // comment symbol | |
| Rust (.rs) - // comment symbol | |
| SQL (.sql) - -- comment symbol | |
| Lua (.lua) - -- comment symbol | |
| VimScript (.vim) - " comment symbol | |
| .PARAMETERS | |
| -File <string> | |
| The path to the file to edit. Creates the file if it doesn't exist. | |
| -DebugMode <switch> | |
| Enable debug mode to log errors to 'mini-nano-debug.log' in the file's directory. | |
| .EXAMPLES | |
| PS> .\mini-nano.ps1 -File "D:\documents\notes.txt" | |
| PS> .\mini-nano.ps1 -File "script.ps1" -DebugMode | |
| PS> .\mini-nano.ps1 "C:\config\settings.ini" | |
| .NOTES | |
| Author: wen | |
| Version: 1.0 | |
| Last Updated: 2025-12-03 | |
| The editor maintains undo/redo history (default: 100 steps) and provides | |
| a terminal-based interface optimized for quick edits without leaving the console. | |
| #> | |
| param( | |
| [Parameter(Mandatory = $true)] | |
| [string]$File, | |
| [switch]$DebugMode | |
| ) | |
| # If Debug mode is enabled, set error log file | |
| if ($DebugMode) { | |
| $fileDir = Split-Path $File -Parent | |
| if ([string]::IsNullOrEmpty($fileDir)) { | |
| $fileDir = Get-Location | |
| } | |
| $ErrorLogFile = Join-Path $fileDir "mini-nano-debug.log" | |
| "=== Debug Log Started at $(Get-Date) ===" | Out-File $ErrorLogFile -Encoding utf8 | |
| $ErrorActionPreference = 'Continue' | |
| } | |
| if (-not (Test-Path $File)) { | |
| "" | Out-File $File -Encoding utf8 | |
| } | |
| $content = Get-Content -Raw -Encoding utf8 $File -ErrorAction SilentlyContinue | |
| if (-not $content) { $content = "" } | |
| # Split content into line array | |
| $textLines = @($content -split "`r?`n") | |
| $currentLine = 0 # Cursor starts from first line | |
| $cursorCol = 0 # Cursor starts from first character | |
| $showLineNumbers = $true # Line number display toggle | |
| $viewStartLine = 0 # Window start line (used for scrolling display) | |
| # Selection range related variables | |
| $selectionStart = $null # @{Line=0; Col=0} | |
| $selectionEnd = $null | |
| $clipboard = "" # Internal clipboard | |
| # Undo/Redo related variables | |
| $undoStack = @() # Store history state | |
| $redoStack = @() # Store redo state | |
| $maxUndoSteps = 100 # Maximum history steps to keep | |
| # Save initial state | |
| function Save-State { | |
| param($lines, $curLine, $curCol) | |
| $state = @{ | |
| Lines = $lines | ForEach-Object { $_ } # Copy array | |
| CursorLine = $curLine | |
| CursorCol = $curCol | |
| } | |
| # Limit undo stack size | |
| if ($undoStack.Count -ge $maxUndoSteps) { | |
| $undoStack = $undoStack[1..($undoStack.Count-1)] | |
| } | |
| $script:undoStack += $state | |
| $script:redoStack = @() # Clear redo stack | |
| } | |
| function Restore-State { | |
| param($state) | |
| return @{ | |
| Lines = $state.Lines | ForEach-Object { $_ } # Copy array | |
| CursorLine = $state.CursorLine | |
| CursorCol = $state.CursorCol | |
| } | |
| } | |
| # Determine comment symbol based on file extension | |
| $fileExt = [System.IO.Path]::GetExtension($File).ToLower() | |
| $commentSymbol = switch ($fileExt) { | |
| ".ps1" { "# " } | |
| ".sh" { "# " } | |
| ".bash" { "# " } | |
| ".py" { "# " } | |
| ".rb" { "# " } | |
| ".yaml" { "# " } | |
| ".yml" { "# " } | |
| ".bat" { "REM " } | |
| ".cmd" { "REM " } | |
| ".js" { "// " } | |
| ".ts" { "// " } | |
| ".cs" { "// " } | |
| ".java" { "// " } | |
| ".cpp" { "// " } | |
| ".c" { "// " } | |
| ".go" { "// " } | |
| ".rs" { "// " } | |
| ".sql" { "-- " } | |
| ".lua" { "-- " } | |
| ".vim" { '" ' } | |
| default { "# " } | |
| } | |
| # Clear screen and display title | |
| Clear-Host | |
| Write-Host "=== mini-nano.ps1 ===" -ForegroundColor Cyan | |
| Write-Host "[F2] Save [Ctrl+Q/ESC] Quit [Ctrl+Z] Undo [Ctrl+A] Select All" -ForegroundColor Yellow | |
| Write-Host "[Ctrl+Alt+X/C/V] Cut/Copy/Paste [PageUp/PageDown] Page Up/Down" -ForegroundColor Yellow | |
| Write-Host "Editing: $File (Comment: $commentSymbol)" | |
| Write-Host "-------------------------------------" | |
| function Get-SelectionRange { | |
| param($start, $end) | |
| if (-not $start -or -not $end) { return $null } | |
| # Ensure start is before end | |
| if ($start.Line -gt $end.Line -or ($start.Line -eq $end.Line -and $start.Col -gt $end.Col)) { | |
| $temp = $start | |
| $start = $end | |
| $end = $temp | |
| } | |
| return @{Start=$start; End=$end} | |
| } | |
| function Get-SelectedText { | |
| param($lines, $range) | |
| if (-not $range) { return "" } | |
| $start = $range.Start | |
| $end = $range.End | |
| if ($start.Line -eq $end.Line) { | |
| # Same line | |
| return $lines[$start.Line].Substring($start.Col, $end.Col - $start.Col) | |
| } else { | |
| # Across multiple lines | |
| $result = @() | |
| $result += $lines[$start.Line].Substring($start.Col) | |
| for ($i = $start.Line + 1; $i -lt $end.Line; $i++) { | |
| $result += $lines[$i] | |
| } | |
| $result += $lines[$end.Line].Substring(0, $end.Col) | |
| return $result -join "`n" | |
| } | |
| } | |
| function Remove-SelectedText { | |
| param($lines, $range) | |
| if (-not $range) { return $lines } | |
| $start = $range.Start | |
| $end = $range.End | |
| if ($start.Line -eq $end.Line) { | |
| # Same line | |
| $line = $lines[$start.Line] | |
| $lines[$start.Line] = $line.Substring(0, $start.Col) + $line.Substring($end.Col) | |
| } else { | |
| # Across multiple lines | |
| $newLine = $lines[$start.Line].Substring(0, $start.Col) + $lines[$end.Line].Substring($end.Col) | |
| # Combine new line array, avoid negative index issues | |
| $beforeLines = if ($start.Line -gt 0) { $lines[0..($start.Line-1)] } else { @() } | |
| $afterLines = if ($end.Line -lt $lines.Count - 1) { $lines[($end.Line+1)..($lines.Count-1)] } else { @() } | |
| if ($beforeLines.Count -eq 0 -and $afterLines.Count -eq 0) { | |
| # Delete all case, keep at least 2 empty lines for stability | |
| $lines = @($newLine, "") | |
| } else { | |
| $lines = $beforeLines + @($newLine) + $afterLines | |
| } | |
| } | |
| # Ensure at least two lines (consistent with Backspace/Delete behavior) | |
| if ($lines.Count -eq 0) { | |
| $lines = @("", "") | |
| } elseif ($lines.Count -eq 1) { | |
| $lines = @($lines[0], "") | |
| } | |
| return $lines | |
| } | |
| function Redraw-Screen { | |
| param($lines, $curLine, $curCol, $showLineNum, $selStart, $selEnd, $viewStart) | |
| $lineNumWidth = if ($showLineNum) { 6 } else { 0 } | |
| # Get selection range | |
| $range = Get-SelectionRange $selStart $selEnd | |
| # Get terminal window height | |
| $windowHeight = $Host.UI.RawUI.WindowSize.Height | |
| $maxContentLines = $windowHeight - 5 # Deduct title and bottom space | |
| # Ensure viewStart is within reasonable range | |
| $viewEnd = $viewStart + $maxContentLines - 1 | |
| # Clear content area and redraw (only draw visible range) | |
| for ($i = 0; $i -lt $maxContentLines; $i++) { | |
| $lineIndex = $viewStart + $i | |
| $Host.UI.RawUI.CursorPosition = @{X=0; Y=$i + 4} | |
| if ($lineIndex -lt $lines.Count) { | |
| $line = $lines[$lineIndex] | |
| if ($showLineNum) { | |
| Write-Host ("{0,3} | " -f ($lineIndex+1)) -NoNewline -ForegroundColor DarkGray | |
| } | |
| # Determine if this line has selection range | |
| if ($range -and $lineIndex -ge $range.Start.Line -and $lineIndex -le $range.End.Line) { | |
| $startCol = if ($lineIndex -eq $range.Start.Line) { $range.Start.Col } else { 0 } | |
| $endCol = if ($lineIndex -eq $range.End.Line) { $range.End.Col } else { $line.Length } | |
| # Display text before selection | |
| if ($startCol -gt 0) { | |
| Write-Host $line.Substring(0, $startCol) -NoNewline | |
| } | |
| # Display selected text (highlighted) | |
| if ($endCol -gt $startCol) { | |
| Write-Host $line.Substring($startCol, $endCol - $startCol) -NoNewline -BackgroundColor DarkBlue -ForegroundColor White | |
| } | |
| # Display text after selection | |
| if ($endCol -lt $line.Length) { | |
| Write-Host $line.Substring($endCol) -NoNewline | |
| } | |
| } else { | |
| Write-Host $line -NoNewline | |
| } | |
| # Clear remaining part of line | |
| $remainingWidth = $Host.UI.RawUI.WindowSize.Width - $line.Length - $lineNumWidth | |
| if ($remainingWidth -gt 0) { | |
| Write-Host (" " * $remainingWidth) -NoNewline | |
| } | |
| } else { | |
| # Clear extra lines (old screen left after deletion) | |
| Write-Host (" " * $Host.UI.RawUI.WindowSize.Width) -NoNewline | |
| } | |
| } | |
| # Position cursor to correct position (relative to window) | |
| $Host.UI.RawUI.CursorPosition = @{ | |
| X = [Math]::Min($curCol + $lineNumWidth, $Host.UI.RawUI.WindowSize.Width - 1) | |
| Y = ($curLine - $viewStart) + 4 | |
| } | |
| } | |
| # Initial draw | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| while ($true) { | |
| try { | |
| # Ensure cursor is visible: adjust window start line | |
| $windowHeight = $Host.UI.RawUI.WindowSize.Height | |
| $maxContentLines = $windowHeight - 5 | |
| if ($currentLine -lt $viewStartLine) { | |
| # Cursor above window, scroll up | |
| $viewStartLine = $currentLine | |
| } elseif ($currentLine -ge $viewStartLine + $maxContentLines) { | |
| # Cursor below window, scroll down | |
| $viewStartLine = $currentLine - $maxContentLines + 1 | |
| } | |
| $key = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") | |
| switch ($key.VirtualKeyCode) { | |
| # F2 -> Save | |
| 113 { | |
| $output = $textLines -join "`n" | |
| $output | Out-File $File -Encoding utf8 -NoNewline | |
| $Host.UI.RawUI.CursorPosition = @{X=0; Y=3} | |
| Write-Host ("[✓] Saved $File" + (" " * 50)) -ForegroundColor Green | |
| Start-Sleep -Milliseconds 500 | |
| $Host.UI.RawUI.CursorPosition = @{X=0; Y=3} | |
| Write-Host ("-------------------------------------" + (" " * 50)) | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } | |
| # Ctrl+Q or ESC -> Quit | |
| { $_ -eq 81 -or $_ -eq 27 } { | |
| $shouldQuit = $false | |
| if ($key.VirtualKeyCode -eq 27) { | |
| # ESC key, exit directly | |
| $shouldQuit = $true | |
| } elseif ($key.ControlKeyState -match "LeftCtrl|RightCtrl") { | |
| # Ctrl+Q | |
| $shouldQuit = $true | |
| } | |
| if ($shouldQuit) { | |
| Clear-Host | |
| Write-Host "[!] Exiting editor..." | |
| break | |
| } elseif ($key.Character -and -not [char]::IsControl($key.Character)) { | |
| # Treat as regular character input (q key) | |
| Save-State $textLines $currentLine $cursorCol | |
| $range = Get-SelectionRange $selectionStart $selectionEnd | |
| if ($range) { | |
| $textLines = Remove-SelectedText $textLines $range | |
| $currentLine = $range.Start.Line | |
| $cursorCol = $range.Start.Col | |
| $selectionStart = $null | |
| $selectionEnd = $null | |
| } | |
| $line = $textLines[$currentLine] | |
| if ($null -eq $line) { $line = "" } | |
| $textLines[$currentLine] = $line.Substring(0, $cursorCol) + $key.Character + $line.Substring($cursorCol) | |
| $cursorCol++ | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } | |
| } | |
| # Ctrl+Alt+X -> Cut | |
| 88 { | |
| if (($key.ControlKeyState -match "LeftCtrl|RightCtrl") -and ($key.ControlKeyState -match "LeftAlt|RightAlt")) { | |
| $range = Get-SelectionRange $selectionStart $selectionEnd | |
| if ($range) { | |
| Save-State $textLines $currentLine $cursorCol | |
| $clipboard = Get-SelectedText $textLines $range | |
| $textLines = Remove-SelectedText $textLines $range | |
| $currentLine = $range.Start.Line | |
| $cursorCol = $range.Start.Col | |
| # Ensure cursor position is valid | |
| $currentLine = [Math]::Max(0, [Math]::Min($currentLine, $textLines.Count - 1)) | |
| $cursorCol = [Math]::Max(0, [Math]::Min($cursorCol, $textLines[$currentLine].Length)) | |
| $selectionStart = $null | |
| $selectionEnd = $null | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } | |
| } elseif ($key.Character -and -not [char]::IsControl($key.Character)) { | |
| # Treat as regular character input | |
| Save-State $textLines $currentLine $cursorCol | |
| $range = Get-SelectionRange $selectionStart $selectionEnd | |
| if ($range) { | |
| $textLines = Remove-SelectedText $textLines $range | |
| $currentLine = $range.Start.Line | |
| $cursorCol = $range.Start.Col | |
| $selectionStart = $null | |
| $selectionEnd = $null | |
| } | |
| $line = $textLines[$currentLine] | |
| if ($null -eq $line) { $line = "" } | |
| $textLines[$currentLine] = $line.Substring(0, $cursorCol) + $key.Character + $line.Substring($cursorCol) | |
| $cursorCol++ | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } | |
| } | |
| # Ctrl+Alt+C -> Copy | |
| 67 { | |
| if (($key.ControlKeyState -match "LeftCtrl|RightCtrl") -and ($key.ControlKeyState -match "LeftAlt|RightAlt")) { | |
| $range = Get-SelectionRange $selectionStart $selectionEnd | |
| if ($range) { | |
| $clipboard = Get-SelectedText $textLines $range | |
| # Don't clear selection, just copy (no undo needed) | |
| } | |
| } elseif ($key.Character -and -not [char]::IsControl($key.Character)) { | |
| # 當作一般字元輸入 | |
| Save-State $textLines $currentLine $cursorCol | |
| $range = Get-SelectionRange $selectionStart $selectionEnd | |
| if ($range) { | |
| $textLines = Remove-SelectedText $textLines $range | |
| $currentLine = $range.Start.Line | |
| $cursorCol = $range.Start.Col | |
| $selectionStart = $null | |
| $selectionEnd = $null | |
| } | |
| $line = $textLines[$currentLine] | |
| if ($null -eq $line) { $line = "" } | |
| $textLines[$currentLine] = $line.Substring(0, $cursorCol) + $key.Character + $line.Substring($cursorCol) | |
| $cursorCol++ | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } | |
| } | |
| # Ctrl+Alt+V -> Paste | |
| 86 { | |
| if (($key.ControlKeyState -match "LeftCtrl|RightCtrl") -and ($key.ControlKeyState -match "LeftAlt|RightAlt")) { | |
| if ($clipboard) { | |
| Save-State $textLines $currentLine $cursorCol | |
| # If there's a selection, delete it first | |
| $range = Get-SelectionRange $selectionStart $selectionEnd | |
| if ($range) { | |
| $textLines = Remove-SelectedText $textLines $range | |
| $currentLine = $range.Start.Line | |
| $cursorCol = $range.Start.Col | |
| } | |
| # Insert clipboard content | |
| $pasteLines = @($clipboard -split "`n") | |
| $line = $textLines[$currentLine] | |
| if ($pasteLines.Count -eq 1) { | |
| # Single line paste | |
| $textLines[$currentLine] = $line.Substring(0, $cursorCol) + $pasteLines[0] + $line.Substring($cursorCol) | |
| $cursorCol += $pasteLines[0].Length | |
| } else { | |
| # Multi-line paste | |
| $firstLine = $line.Substring(0, $cursorCol) + $pasteLines[0] | |
| $lastLine = $pasteLines[-1] + $line.Substring($cursorCol) | |
| $middleLines = $pasteLines[1..($pasteLines.Count-2)] | |
| # Combine new line array, avoid negative index issues | |
| $beforeLines = if ($currentLine -gt 0) { $textLines[0..($currentLine-1)] } else { @() } | |
| $afterLines = if ($currentLine -lt $textLines.Count - 1) { $textLines[($currentLine+1)..($textLines.Count-1)] } else { @() } | |
| $textLines = $beforeLines + @($firstLine) + $middleLines + @($lastLine) + $afterLines | |
| $currentLine += $pasteLines.Count - 1 | |
| $cursorCol = $pasteLines[-1].Length | |
| } | |
| $selectionStart = $null | |
| $selectionEnd = $null | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } | |
| } elseif ($key.Character -and -not [char]::IsControl($key.Character)) { | |
| # 當作一般字元輸入 | |
| Save-State $textLines $currentLine $cursorCol | |
| $range = Get-SelectionRange $selectionStart $selectionEnd | |
| if ($range) { | |
| $textLines = Remove-SelectedText $textLines $range | |
| $currentLine = $range.Start.Line | |
| $cursorCol = $range.Start.Col | |
| $selectionStart = $null | |
| $selectionEnd = $null | |
| } | |
| $line = $textLines[$currentLine] | |
| if ($null -eq $line) { $line = "" } | |
| $textLines[$currentLine] = $line.Substring(0, $cursorCol) + $key.Character + $line.Substring($cursorCol) | |
| $cursorCol++ | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } | |
| } | |
| # Ctrl+Z -> Undo | |
| 90 { | |
| if ($key.ControlKeyState -match "LeftCtrl|RightCtrl") { | |
| if ($undoStack.Count -gt 0) { | |
| # Save current state to redo stack | |
| $currentState = @{ | |
| Lines = $textLines | ForEach-Object { $_ } | |
| CursorLine = $currentLine | |
| CursorCol = $cursorCol | |
| } | |
| $redoStack += $currentState | |
| # Restore previous state | |
| $lastState = $undoStack[-1] | |
| $undoStack = $undoStack[0..($undoStack.Count-2)] | |
| $restored = Restore-State $lastState | |
| $textLines = $restored.Lines | |
| $currentLine = $restored.CursorLine | |
| $cursorCol = $restored.CursorCol | |
| $selectionStart = $null | |
| $selectionEnd = $null | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } | |
| } elseif ($key.Character -and -not [char]::IsControl($key.Character)) { | |
| # 當作一般字元輸入 | |
| Save-State $textLines $currentLine $cursorCol | |
| $range = Get-SelectionRange $selectionStart $selectionEnd | |
| if ($range) { | |
| $textLines = Remove-SelectedText $textLines $range | |
| $currentLine = $range.Start.Line | |
| $cursorCol = $range.Start.Col | |
| $selectionStart = $null | |
| $selectionEnd = $null | |
| } | |
| $line = $textLines[$currentLine] | |
| if ($null -eq $line) { $line = "" } | |
| $textLines[$currentLine] = $line.Substring(0, $cursorCol) + $key.Character + $line.Substring($cursorCol) | |
| $cursorCol++ | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } | |
| } | |
| # Ctrl+A -> Select All | |
| 65 { | |
| if ($key.ControlKeyState -match "LeftCtrl|RightCtrl") { | |
| $selectionStart = @{Line=0; Col=0} | |
| $selectionEnd = @{Line=$textLines.Count-1; Col=$textLines[-1].Length} | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } elseif ($key.Character -and -not [char]::IsControl($key.Character)) { | |
| # 當作一般字元輸入 | |
| Save-State $textLines $currentLine $cursorCol | |
| $range = Get-SelectionRange $selectionStart $selectionEnd | |
| if ($range) { | |
| $textLines = Remove-SelectedText $textLines $range | |
| $currentLine = $range.Start.Line | |
| $cursorCol = $range.Start.Col | |
| $selectionStart = $null | |
| $selectionEnd = $null | |
| } | |
| $line = $textLines[$currentLine] | |
| if ($null -eq $line) { $line = "" } | |
| $textLines[$currentLine] = $line.Substring(0, $cursorCol) + $key.Character + $line.Substring($cursorCol) | |
| $cursorCol++ | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } | |
| } | |
| # Ctrl+L -> Toggle line number display | |
| 76 { | |
| if ($key.ControlKeyState -match "LeftCtrl|RightCtrl") { | |
| $showLineNumbers = -not $showLineNumbers | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } elseif ($key.Character -and -not [char]::IsControl($key.Character)) { | |
| # 當作一般字元輸入 | |
| Save-State $textLines $currentLine $cursorCol | |
| $range = Get-SelectionRange $selectionStart $selectionEnd | |
| if ($range) { | |
| $textLines = Remove-SelectedText $textLines $range | |
| $currentLine = $range.Start.Line | |
| $cursorCol = $range.Start.Col | |
| $selectionStart = $null | |
| $selectionEnd = $null | |
| } | |
| $line = $textLines[$currentLine] | |
| if ($null -eq $line) { $line = "" } | |
| $textLines[$currentLine] = $line.Substring(0, $cursorCol) + $key.Character + $line.Substring($cursorCol) | |
| $cursorCol++ | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } | |
| } | |
| # Ctrl+/ (191) -> Toggle line comment | |
| 191 { | |
| if ($key.ControlKeyState -match "LeftCtrl|RightCtrl") { | |
| Save-State $textLines $currentLine $cursorCol | |
| $line = $textLines[$currentLine] | |
| $trimmedLine = $line.TrimStart() | |
| # Check if already commented | |
| if ($trimmedLine.StartsWith($commentSymbol)) { | |
| # Remove comment | |
| $leadingSpaces = $line.Length - $trimmedLine.Length | |
| $newLine = $line.Substring(0, $leadingSpaces) + $trimmedLine.Substring($commentSymbol.Length) | |
| $textLines[$currentLine] = $newLine | |
| # Adjust cursor position | |
| if ($cursorCol -gt $leadingSpaces + $commentSymbol.Length) { | |
| $cursorCol = [Math]::Max(0, $cursorCol - $commentSymbol.Length) | |
| } | |
| } else { | |
| # Add comment | |
| $leadingSpaces = $line.Length - $trimmedLine.Length | |
| $newLine = $line.Substring(0, $leadingSpaces) + $commentSymbol + $trimmedLine | |
| $textLines[$currentLine] = $newLine | |
| # Adjust cursor position | |
| if ($cursorCol -ge $leadingSpaces) { | |
| $cursorCol += $commentSymbol.Length | |
| } | |
| } | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } elseif ($key.Character -and -not [char]::IsControl($key.Character)) { | |
| # 當作一般字元輸入 | |
| Save-State $textLines $currentLine $cursorCol | |
| $range = Get-SelectionRange $selectionStart $selectionEnd | |
| if ($range) { | |
| $textLines = Remove-SelectedText $textLines $range | |
| $currentLine = $range.Start.Line | |
| $cursorCol = $range.Start.Col | |
| $selectionStart = $null | |
| $selectionEnd = $null | |
| } | |
| $line = $textLines[$currentLine] | |
| if ($null -eq $line) { $line = "" } | |
| $textLines[$currentLine] = $line.Substring(0, $cursorCol) + $key.Character + $line.Substring($cursorCol) | |
| $cursorCol++ | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } | |
| } | |
| # Page Up (33) -> Page Up | |
| # Page Up (33) -> Page Up | |
| 33 { | |
| # Check if Shift is pressed | |
| $isShiftPressed = $key.ControlKeyState -match "ShiftPressed" | |
| $windowHeight = $Host.UI.RawUI.WindowSize.Height | |
| $maxContentLines = $windowHeight - 5 | |
| if ($isShiftPressed -and -not $selectionStart) { | |
| # Start selection | |
| $selectionStart = @{Line=$currentLine; Col=$cursorCol} | |
| } | |
| # 計算新的游標位置 | |
| $newLine = [Math]::Max(0, $currentLine - $maxContentLines) | |
| $currentLine = $newLine | |
| $cursorCol = [Math]::Min($cursorCol, $textLines[$currentLine].Length) | |
| if ($isShiftPressed) { | |
| $selectionEnd = @{Line=$currentLine; Col=$cursorCol} | |
| } else { | |
| # Clear selection | |
| $selectionStart = $null | |
| $selectionEnd = $null | |
| } | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } | |
| # Page Down (34) -> Page Down | |
| 34 { | |
| # Check if Shift is pressed | |
| $isShiftPressed = $key.ControlKeyState -match "ShiftPressed" | |
| $windowHeight = $Host.UI.RawUI.WindowSize.Height | |
| $maxContentLines = $windowHeight - 5 | |
| if ($isShiftPressed -and -not $selectionStart) { | |
| # Start selection | |
| $selectionStart = @{Line=$currentLine; Col=$cursorCol} | |
| } | |
| # 計算新的游標位置 | |
| $newLine = [Math]::Min($textLines.Count - 1, $currentLine + $maxContentLines) | |
| $currentLine = $newLine | |
| $cursorCol = [Math]::Min($cursorCol, $textLines[$currentLine].Length) | |
| if ($isShiftPressed) { | |
| $selectionEnd = @{Line=$currentLine; Col=$cursorCol} | |
| } else { | |
| # 清除選取 | |
| $selectionStart = $null | |
| $selectionEnd = $null | |
| } | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } | |
| # Up Arrow (38) | |
| 38 { | |
| # Check if Shift is pressed | |
| $isShiftPressed = $key.ControlKeyState -match "ShiftPressed" | |
| if ($currentLine -gt 0) { | |
| if ($isShiftPressed -and -not $selectionStart) { | |
| # Start selection | |
| $selectionStart = @{Line=$currentLine; Col=$cursorCol} | |
| } | |
| $currentLine-- | |
| $cursorCol = [Math]::Min($cursorCol, $textLines[$currentLine].Length) | |
| if ($isShiftPressed) { | |
| $selectionEnd = @{Line=$currentLine; Col=$cursorCol} | |
| } else { | |
| $selectionStart = $null | |
| $selectionEnd = $null | |
| } | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } | |
| } | |
| # Down Arrow (40) | |
| 40 { | |
| # Check if Shift is pressed | |
| $isShiftPressed = $key.ControlKeyState -match "ShiftPressed" | |
| if ($currentLine -lt $textLines.Count - 1) { | |
| if ($isShiftPressed -and -not $selectionStart) { | |
| # Start selection | |
| $selectionStart = @{Line=$currentLine; Col=$cursorCol} | |
| } | |
| $currentLine++ | |
| $cursorCol = [Math]::Min($cursorCol, $textLines[$currentLine].Length) | |
| if ($isShiftPressed) { | |
| $selectionEnd = @{Line=$currentLine; Col=$cursorCol} | |
| } else { | |
| $selectionStart = $null | |
| $selectionEnd = $null | |
| } | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } | |
| } | |
| # Left Arrow (37) | |
| 37 { | |
| # 檢查是否按住 Shift | |
| $isShiftPressed = $key.ControlKeyState -match "ShiftPressed" | |
| if ($cursorCol -gt 0) { | |
| if ($isShiftPressed -and -not $selectionStart) { | |
| $selectionStart = @{Line=$currentLine; Col=$cursorCol} | |
| } | |
| $cursorCol-- | |
| if ($isShiftPressed) { | |
| $selectionEnd = @{Line=$currentLine; Col=$cursorCol} | |
| } else { | |
| $selectionStart = $null | |
| $selectionEnd = $null | |
| } | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } elseif ($currentLine -gt 0) { | |
| if ($isShiftPressed -and -not $selectionStart) { | |
| $selectionStart = @{Line=$currentLine; Col=$cursorCol} | |
| } | |
| $currentLine-- | |
| $cursorCol = $textLines[$currentLine].Length | |
| if ($isShiftPressed) { | |
| $selectionEnd = @{Line=$currentLine; Col=$cursorCol} | |
| } else { | |
| $selectionStart = $null | |
| $selectionEnd = $null | |
| } | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } | |
| } | |
| # Right Arrow (39) | |
| 39 { | |
| # 檢查是否按住 Shift | |
| $isShiftPressed = $key.ControlKeyState -match "ShiftPressed" | |
| if ($cursorCol -lt $textLines[$currentLine].Length) { | |
| if ($isShiftPressed -and -not $selectionStart) { | |
| $selectionStart = @{Line=$currentLine; Col=$cursorCol} | |
| } | |
| $cursorCol++ | |
| if ($isShiftPressed) { | |
| $selectionEnd = @{Line=$currentLine; Col=$cursorCol} | |
| } else { | |
| $selectionStart = $null | |
| $selectionEnd = $null | |
| } | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } elseif ($currentLine -lt $textLines.Count - 1) { | |
| if ($isShiftPressed -and -not $selectionStart) { | |
| $selectionStart = @{Line=$currentLine; Col=$cursorCol} | |
| } | |
| $currentLine++ | |
| $cursorCol = 0 | |
| if ($isShiftPressed) { | |
| $selectionEnd = @{Line=$currentLine; Col=$cursorCol} | |
| } else { | |
| $selectionStart = $null | |
| $selectionEnd = $null | |
| } | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } | |
| } | |
| # Backspace | |
| 8 { | |
| # 如果有選取範圍,先刪除選取範圍 | |
| $range = Get-SelectionRange $selectionStart $selectionEnd | |
| if ($range) { | |
| Save-State $textLines $currentLine $cursorCol | |
| $textLines = Remove-SelectedText $textLines $range | |
| $currentLine = $range.Start.Line | |
| $cursorCol = $range.Start.Col | |
| # 確保游標位置有效 | |
| $currentLine = [Math]::Max(0, [Math]::Min($currentLine, $textLines.Count - 1)) | |
| $cursorCol = [Math]::Max(0, [Math]::Min($cursorCol, $textLines[$currentLine].Length)) | |
| $selectionStart = $null | |
| $selectionEnd = $null | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } elseif ($cursorCol -gt 0) { | |
| Save-State $textLines $currentLine $cursorCol | |
| $line = $textLines[$currentLine] | |
| if ($null -eq $line) { $line = "" } | |
| $textLines[$currentLine] = $line.Substring(0, $cursorCol - 1) + $line.Substring($cursorCol) | |
| $cursorCol-- | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } elseif ($currentLine -gt 0) { | |
| Save-State $textLines $currentLine $cursorCol | |
| # Merge with previous line | |
| $cursorCol = $textLines[$currentLine - 1].Length | |
| $textLines[$currentLine - 1] += $textLines[$currentLine] | |
| $textLines = $textLines[0..($currentLine-1)] + $textLines[($currentLine+1)..($textLines.Count-1)] | |
| # Ensure at least one line | |
| if ($textLines.Count -eq 0) { | |
| $textLines = @("") | |
| } | |
| $currentLine-- | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } | |
| } | |
| # Enter Key | |
| 13 { | |
| Save-State $textLines $currentLine $cursorCol | |
| # If there's a selection, delete it first | |
| $range = Get-SelectionRange $selectionStart $selectionEnd | |
| if ($range) { | |
| $textLines = Remove-SelectedText $textLines $range | |
| $currentLine = $range.Start.Line | |
| $cursorCol = $range.Start.Col | |
| $selectionStart = $null | |
| $selectionEnd = $null | |
| } | |
| $line = $textLines[$currentLine] | |
| if ($null -eq $line) { $line = "" } | |
| $textLines[$currentLine] = $line.Substring(0, $cursorCol) | |
| $newLine = $line.Substring($cursorCol) | |
| # Combine new line array, avoid negative index issues | |
| $beforeLines = $textLines[0..$currentLine] | |
| $afterLines = if ($currentLine -lt $textLines.Count - 1) { $textLines[($currentLine+1)..($textLines.Count-1)] } else { @() } | |
| $textLines = $beforeLines + @($newLine) + $afterLines | |
| $currentLine++ | |
| $cursorCol = 0 | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } | |
| # Tab Key | |
| 9 { | |
| Save-State $textLines $currentLine $cursorCol | |
| # If there's a selection, delete it first | |
| $range = Get-SelectionRange $selectionStart $selectionEnd | |
| if ($range) { | |
| $textLines = Remove-SelectedText $textLines $range | |
| $currentLine = $range.Start.Line | |
| $cursorCol = $range.Start.Col | |
| $selectionStart = $null | |
| $selectionEnd = $null | |
| } | |
| $line = $textLines[$currentLine] | |
| if ($null -eq $line) { $line = "" } | |
| $textLines[$currentLine] = $line.Substring(0, $cursorCol) + " " + $line.Substring($cursorCol) | |
| $cursorCol += 4 | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } | |
| # Delete Key (46) | |
| 46 { | |
| # 如果有選取範圍,先刪除選取範圍 | |
| $range = Get-SelectionRange $selectionStart $selectionEnd | |
| if ($range) { | |
| Save-State $textLines $currentLine $cursorCol | |
| $textLines = Remove-SelectedText $textLines $range | |
| $currentLine = $range.Start.Line | |
| $cursorCol = $range.Start.Col | |
| # 確保游標位置有效 | |
| $currentLine = [Math]::Max(0, [Math]::Min($currentLine, $textLines.Count - 1)) | |
| $cursorCol = [Math]::Max(0, [Math]::Min($cursorCol, $textLines[$currentLine].Length)) | |
| $selectionStart = $null | |
| $selectionEnd = $null | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } else { | |
| $line = $textLines[$currentLine] | |
| if ($cursorCol -lt $line.Length) { | |
| Save-State $textLines $currentLine $cursorCol | |
| $textLines[$currentLine] = $line.Substring(0, $cursorCol) + $line.Substring($cursorCol + 1) | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } elseif ($currentLine -lt $textLines.Count - 1) { | |
| Save-State $textLines $currentLine $cursorCol | |
| # Merge with next line | |
| $textLines[$currentLine] += $textLines[$currentLine + 1] | |
| $textLines = $textLines[0..$currentLine] + $textLines[($currentLine+2)..($textLines.Count-1)] | |
| # 確保至少有一行 | |
| if ($textLines.Count -eq 0) { | |
| $textLines = @("") | |
| } | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } | |
| } | |
| } | |
| # Home Key (36) | |
| 36 { | |
| # 檢查是否按住 Shift | |
| $isShiftPressed = $key.ControlKeyState -match "ShiftPressed" | |
| if ($isShiftPressed -and -not $selectionStart) { | |
| # 開始選取 | |
| $selectionStart = @{Line=$currentLine; Col=$cursorCol} | |
| } | |
| $cursorCol = 0 | |
| if ($isShiftPressed) { | |
| $selectionEnd = @{Line=$currentLine; Col=$cursorCol} | |
| } else { | |
| $selectionStart = $null | |
| $selectionEnd = $null | |
| } | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } | |
| # End Key (35) | |
| 35 { | |
| # 檢查是否按住 Shift | |
| $isShiftPressed = $key.ControlKeyState -match "ShiftPressed" | |
| if ($isShiftPressed -and -not $selectionStart) { | |
| # 開始選取 | |
| $selectionStart = @{Line=$currentLine; Col=$cursorCol} | |
| } | |
| $cursorCol = $textLines[$currentLine].Length | |
| if ($isShiftPressed) { | |
| $selectionEnd = @{Line=$currentLine; Col=$cursorCol} | |
| } else { | |
| $selectionStart = $null | |
| $selectionEnd = $null | |
| } | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } | |
| default { | |
| if ($key.Character -and -not [char]::IsControl($key.Character)) { | |
| Save-State $textLines $currentLine $cursorCol | |
| # If there's a selection, delete it first | |
| $range = Get-SelectionRange $selectionStart $selectionEnd | |
| if ($range) { | |
| $textLines = Remove-SelectedText $textLines $range | |
| $currentLine = $range.Start.Line | |
| $cursorCol = $range.Start.Col | |
| $selectionStart = $null | |
| $selectionEnd = $null | |
| } | |
| $line = $textLines[$currentLine] | |
| if ($null -eq $line) { $line = "" } | |
| $textLines[$currentLine] = $line.Substring(0, $cursorCol) + $key.Character + $line.Substring($cursorCol) | |
| $cursorCol++ | |
| Redraw-Screen $textLines $currentLine $cursorCol $showLineNumbers $selectionStart $selectionEnd $viewStartLine | |
| } | |
| } | |
| } | |
| if ($key.VirtualKeyCode -eq 81 -and $key.ControlKeyState -match "LeftCtrl|RightCtrl") { break } | |
| if ($key.VirtualKeyCode -eq 27) { break } # ESC can also exit | |
| } catch { | |
| if ($DebugMode) { | |
| $errorInfo = @" | |
| [$(Get-Date)] Error occurred: | |
| Message: $($_.Exception.Message) | |
| Line: $($_.InvocationInfo.ScriptLineNumber) | |
| CurrentLine: $currentLine | |
| CursorCol: $cursorCol | |
| TextLines.Count: $($textLines.Count) | |
| TextLines[$currentLine]: $($textLines[$currentLine]) | |
| StackTrace: $($_.ScriptStackTrace) | |
| --- | |
| "@ | |
| $errorInfo | Out-File $ErrorLogFile -Append -Encoding utf8 | |
| } | |
| # Continue execution, don't break | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment