PowerShell, break out of a timer with custom keypress

317 Views Asked by At

My timer below will countdown from $total_time down to 0, displaying a progress bar. This is fine, but how can I exit the loop and continue the script upon a custom keypress, such as 'ESC', or 'Enter', or 'Space', or 'm'?

How would I then have multiple custom exit options, e.g. 'x' would exit the loop and continue the script, 'p' to exit the loop and do some task (i.e. capture the key that was pressed and test on that for additional tasks), or other ways to manipulate during the countdown?

$total_time = 17   # Seconds in total to countdown
$interval = $total_time / 100   # There are always 100 percentage pips
$ms_per_pip = $interval * 1000

For ($i=0; $i -le 100; $i++) {   # Always 100 pips
    Start-Sleep -Milliseconds $ms_per_pip
    $remaining_time = [math]::Round($total_time - ($i * $ms_per_pip / 1000),2)
    Write-Progress -Activity "Sleeping For $total_time Seconds ($i% complete, $remaining_time Seconds left)" -Status "StatusString" -PercentComplete $i -CurrentOperation "CurrentOperationString"
}
2

There are 2 best solutions below

0
Santiago Squarzon On BEST ANSWER

You can use Console.KeyAvailable to check whether a key press is available in the input stream and then Console.ReadKey(bool) to get the pressed key, after that it's a simple if condition to check if the pressed key is in one of those characters of interest to signal the break of the loop. After that you can use a switch to act on the pressed key.

for($i = 0; $i -le 100; $i++) {
    if([Console]::KeyAvailable) {
        $key = [Console]::ReadKey($true).Key
        if($key -in 'X', 'P') {
            break
        }
    }
    
    # rest of code here
}

switch($key) {
    X {
        'X was pressed'
        # do something with X
    }
    P {
        'P was pressed'
        # do something with P
    }
}
0
pangratt12345 On

Other solutions/answers provided here are great,
but I just wanted to provide fully working standalone example script with a bit different approach.
In original solution timer may block main Gui thread so in below example I made the timer countdown in separate task thread so that other tasks can still be done in main thread.
In original solution, after pressing correct key button, user may need to wait for the interval to fully elapse until the scripts wakes from sleep and responds. For longer timer durations the interval can get longer (1% of total timer's duration time).
I made a modification in example below so that user can break out of a timer's loop instantly after pressing correct key button.

Function InterruptibleTimer([long] $timer_duration_in_seconds, [string] $status_string, [string] $current_operation_string) 
{
    [long] $timer_interval_in_seconds = 1;
    [long] $timer_interval_in_milliseconds = $timer_interval_in_seconds * 1000;
    [System.DateTime] $start_time = Get-Date;
    $pressed_key = $null;
    
    [System.Timers.Timer] $timer = New-Object System.Timers.Timer;
    $timer.Interval = $timer_interval_in_milliseconds;
    #$timer.AutoReset = $true; # default is $true to prevent timer from stopping after reaching first interval
    
    [System.Collections.Hashtable] $timer_data = 
    @{ 
        StartTime = $start_time;
        TimerDurationInSeconds = $timer_duration_in_seconds;
        StatusString = $status_string;
        CurrentOperationString = $current_operation_string;
    };
         
    [System.Management.Automation.ScriptBlock] $timer_instruction_block = 
    { 
        $start_time = $Event.MessageData.StartTime;
        $timer_duration_in_seconds = $Event.MessageData.TimerDurationInSeconds;
        $status_string = $Event.MessageData.StatusString;
        $current_operation_string = $Event.MessageData.CurrentOperationString;

        [System.DateTime] $current_time = Get-Date; 
        [System.TimeSpan] $elapsed_time = $current_time - $start_time;
        [long] $elapsed_seconds = [math]::Floor($elapsed_time.TotalSeconds);
        [long] $remaining_seconds = $timer_duration_in_seconds - $elapsed_seconds;
        [double] $elapsed_percentage = ($elapsed_seconds / $timer_duration_in_seconds) * 100;
        
        Write-Progress -PercentComplete $elapsed_percentage -Status "$status_string" -CurrentOperation "$current_operation_string" -Activity "Timer duration in seconds: $timer_duration_in_seconds s, Elapsed seconds: $elapsed_seconds s, Remaining seconds: $remaining_seconds s, Elapsed percentage: $($elapsed_percentage.ToString("F2"))% complete";
    };
    
    $timer_task = Register-ObjectEvent -InputObject $timer -EventName Elapsed -Action $timer_instruction_block -MessageData $timer_data;

    $timer.Start(); # Start the timer
    Write-Host "Timer has been started";
    
    try {
        Write-Host "Awaiting user's key press or waiting until timer reaches its full duration"
        while ($true) {
            [System.DateTime] $current_time = Get-Date; 
            [System.TimeSpan] $elapsed_time = $current_time - $start_time;
            [long] $elapsed_seconds = [math]::Floor($elapsed_time.TotalSeconds);
            
            if($elapsed_seconds -ge $timer_duration_in_seconds) {
                break;
            }
        
            if([System.Console]::KeyAvailable) {
                $pressed_key = [System.Console]::ReadKey($true).Key;
                Write-Host "Pressed key: $pressed_key";

                if($pressed_key -in 'x', 'p', 'm', 'Enter', 'Spacebar', 'Escape') {
                    break;
                } else {
                    Write-Host "To stop the timer now, press one of the following buttons:";
                    Write-Host "x, p, m, Enter, Space, Esc:";   
                }
            }
        }
    } finally {
        $timer.Stop();
        Remove-Job $timer_task -Force;
        Write-Host "Timer has been stopped";

        switch -CaseSensitive ($pressed_key) {
            $null { Write-Host 'No key was pressed'; }
            'X' { Write-Host 'Exiting the loop and continuing the script'; }
            'P' { Write-Host 'Exiting the loop and doing some tasks'; }
            { @('M', 'Enter', 'Spacebar', 'Escape') -contains $_ } { Write-Host 'Exiting the loop'; }
        }
    }
    return $pressed_key;
}

[long] $TimerDurationInSeconds = 5 * 60; # 5 minutes timer's duration
[string] $StatusString = "Status String";
[string] $CurrentOperationString = "Current Operation String";

Write-Host "Starting the timer which will stop automatically after $TimerDurationInSeconds seconds";
Write-Host "To stop the timer now, press one of the following buttons:";
Write-Host "x, p, m, Enter, Space, Esc:";

$pressed_key = InterruptibleTimer -timer_duration_in_seconds $TimerDurationInSeconds -status_string $StatusString -current_operation_string $CurrentOperationString;