HotKeys.ps1

# HotKeys.ps1 - Define custom hotkeys for PSReadLine

# Create a shortcut variable for easier access to PSReadLine methods in hotkey script blocks.
$script:PSRL = [Microsoft.PowerShell.PSConsoleReadLine]

function Get-InputFromPSReadLine {
    $line = $null
    $cursor = $null
    # Get the current command line and cursor position
    $script:PSRL::GetBufferState([ref]$line, [ref]$cursor)
    if ($null -ne $line -and -not [string]::IsNullOrWhiteSpace($line)) {
        $target = $line.Trim()
        return $target
    }
    return $null
}

# This script sets up custom hotkeys in PSReadLine for enhanced command line editing and history management.
# Set up command prediction to use history and display predictions in a list view style.
if ($host.Name -eq 'ConsoleHost') {
    if ($PSVersionTable.PSVersion -ge [Version]"7.2") {
        # Enable command prediction based on history
        Set-PSReadLineOption -PredictionSource HistoryAndPlugin
    }
    else {
        # For older versions, just use history for prediction
        Set-PSReadLineOption -PredictionSource History
    }

    # Set the prediction view style to ListView for better visibility of suggestions.
    Set-PSReadLineOption -PredictionViewStyle ListView

    # Configure the Tab key to trigger menu completion, which allows cycling through possible completions in a dropdown menu.
    Set-PSReadLineKeyHandler -Key "Tab" -Function MenuComplete

    # Configure Up and Down arrow keys to move the cursor to the end of the line when searching history, which is more intuitive for most users.
    Set-PSReadLineOption -HistorySearchCursorMovesToEnd

    # Set Up and Down arrow keys to search through command history based on the current input, similar to typical shell behavior.
    Set-PSReadLineKeyHandler -Key UpArrow -Function HistorySearchBackward 
    Set-PSReadLineKeyHandler -Key DownArrow -Function HistorySearchForward

    # This script sets up a custom hotkey (Alt+Delete) in PSReadLine to delete the current command line from history.
    # It works by directly manipulating the history file and then refreshing the PSReadLine cache.
    Set-PSReadLineKeyHandler -Chord 'Alt+Delete' `
        -BriefDescription "DeleteFromHistory" `
        -LongDescription "Delete the current command line from history" `
        -ScriptBlock {
        # Yes, this is a hack, but PSReadLine does not provide a direct API to check dropdown visibility
        $options = $script:PSRL::GetOptions()
        $isPredictorOn = ($options.PredictionSource -ne 'None')
    
        $target = Get-InputFromPSReadLine
        # Only proceed if there is a non-empty command line
        if ($null -ne $target) {
            $historyPath = $options.HistorySavePath
            if (Test-Path $historyPath) {
                # remove the target command from history content
                $newContent = [System.IO.File]::ReadAllLines($historyPath) | Where-Object { $_ -ne $target }
            
                # move original history file to a backup location to prevent conflicts during clearing
                Move-Item $historyPath "$historyPath.bak" -Force -ErrorAction SilentlyContinue
            
                # Clear history using PSReadLine API
                $script:PSRL::RevertLine()
                try {
                    $script:PSRL::ClearHistory()
                
                    # Rebuild history file and memory cache using AddToHistory
                    if ($null -ne $newContent) {
                        foreach ($h in $newContent) { 
                            $script:PSRL::AddToHistory($h) 
                        }
                    }
                    # Cleanup backup file
                    Remove-Item "$historyPath.bak" -ErrorAction SilentlyContinue
                }
                catch {
                    # In case of any error, restore the original history file to prevent data loss
                    Move-Item "$historyPath.bak" $historyPath -Force -ErrorAction SilentlyContinue
                }

                # If predictor is on, we need to trigger it to refresh its cache after history change
                if ($isPredictorOn) {
                    # Trigger a dummy input to refresh predictor cache
                    $script:PSRL::Insert(' ')
                    $script:PSRL::BackwardDeleteChar()
                }
            }
        }
    }

    Set-PSReadLineKeyHandler -Chord 'Alt+s' `
        -BriefDescription "SaveInHistory" `
        -LongDescription "Save the current command line in history but do not execute it" `
        -ScriptBlock {
        $target = Get-InputFromPSReadLine
        # Only proceed if there is a non-empty command line
        if ($null -ne $target) {
            $script:PSRL::AddToHistory($target)
        }
        $script:PSRL::RevertLine()
    }
}