Skip to content

Instantly share code, notes, and snippets.

@superyngo
Last active December 31, 2025 01:11
Show Gist options
  • Select an option

  • Save superyngo/c77bbbf67759cc4213aac12ebe980225 to your computer and use it in GitHub Desktop.

Select an option

Save superyngo/c77bbbf67759cc4213aac12ebe980225 to your computer and use it in GitHub Desktop.
Shell and PowerShell utility scripts
<#
.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