Home > Software engineering >  Command Line Countdown Timer Skipping a Second
Command Line Countdown Timer Skipping a Second

Time:10-30

I've been looking to add a simple countdown timer to my script. I found this one that seemed to do the trick and modified it slightly to only display seconds since I don't need anything more than that.

When I run it, it will skip the 2nd second in the countdown. For example, if I ran Start-CountdownTimer -Seconds 10, the output will be (this is split into separate lines for demo purposes since it'll be on the same line):

 |  Starting in 10s ...
 /  Starting in 8s ...
 -  Starting in 7s ...
 \  Starting in 6s ...
 |  Starting in 5s ...
 /  Starting in 4s ...
 -  Starting in 3s ...
 \  Starting in 2s ...
 |  Starting in 1s ...
 /  Starting in 0s ...

Any ideas how I can fix this? This is the (slightly modified) code from the link above:

Function Start-CountdownTimer{
param (
    <#[int]$Days = 0,
    [int]$Hours = 0,
    [int]$Minutes = 0,#>
    [int]$Seconds = 0,
    [int]$TickLength = 1
)
$t = New-TimeSpan <#-Days $Days -Hours $Hours -Minutes $Minutes#> -Seconds $Seconds
$origpos = $host.UI.RawUI.CursorPosition
$spinner =@('|', '/', '-', '\')
$spinnerPos = 0
$remain = $t
$d =( get-date)   $t
$remain = ($d - (get-date))
while ($remain.TotalSeconds -gt 0){
  Write-Host (" {0} " -f $spinner[$spinnerPos%4]) -ForegroundColor White -NoNewline
  write-Host (" Starting in {0:d1}s ..." -f $remain.Seconds)
  $host.UI.RawUI.CursorPosition = $origpos
  $spinnerPos  = 1
  Start-Sleep -seconds $TickLength
  $remain = ($d - (get-date))
}
$host.UI.RawUI.CursorPosition = $origpos
}

CodePudding user response:

Well this should be an improvement to your function and also should solve the problem of jumping an extra second back. I personally do not agree with using $host and would use Clear-Host instead so the function is compatible with PowerShell ISE too but you can change that as you please.

Function Start-CountdownTimer{
param(
    [CmdletBinding(DefaultParameterSetName = 'Time')]
    [parameter(ParameterSetName = 'Hours')]
    [ValidateRange(1, [int]::MaxValue)]
    [int]$Hours = 0,
    [parameter(ParameterSetName = 'Minutes')]
    [ValidateRange(1, [int]::MaxValue)]
    [int]$Minutes = 0,
    [parameter(ParameterSetName = 'Seconds', Position = 0)]
    [ValidateRange(1, [int]::MaxValue)]
    [int]$Seconds = 0
)

    $now = [datetime]::Now
    $spinner = @('|', '/', '-', '\')
    $spinnerPos = 0
    $origpos = $host.UI.RawUI.CursorPosition

    $runTime = switch($PSBoundParameters.Keys)
    {
        Hours   { $now.AddHours($PSBoundParameters[$PSBoundParameters.Keys]) }
        Minutes { $now.AddMinutes($PSBoundParameters[$PSBoundParameters.Keys]) }
        Seconds { $now.AddSeconds($PSBoundParameters[$PSBoundParameters.Keys]) }
    }

    $runTime = $runTime.TimeOfDay.TotalSeconds - $now.TimeOfDay.TotalSeconds

    while($runTime)
    {
        Write-Host (" {0} " -f $spinner[$spinnerPos%4]) -NoNewline
        Write-Host " Starting in ${runTime}s ..."
        $host.UI.RawUI.CursorPosition = $origpos
        $spinnerPos  = 1
        $runTime--

        Start-Sleep -Seconds 1
    }
}

CodePudding user response:

  • If you want a solution that never skips a countdown step - at the expense of running a bit longer than the specified countdown duration (increasingly so, the longer the duration) - see Santiago Squarzon's helpful answer.

  • If you want a solution that may skip a countdown step, under heavy system load, but remains close to the specified countdown duration, irrespective of its length, see below.


As Theo points out, your implementation is somewhat inefficient, which increases the risk of skipping a step.

The following is a streamlined implementation that lessens the risk of skipping, though under heavy system load it may still occur.

Note:

  • The function does not work properly in the Windows PowerShell ISE, but the ISE is obsolescent and there have always been reasons not to use it (bottom section); by contrast, the function does work in the ISE's successor, Visual Studio Code with its PowerShell extension

  • Instead of using $host.UI.RawUI, a simple CR ("`r") is issued at the start of each countdown step's Write-Host -NoNewLine call, which causes the cursor to return to the start of the line and overwrite the existing line.

  • To avoid flickering when the line is overwritten, the cursor is temporarily turned off, using [Console]::CursorVisible]

  • Strictly speaking, based on PowerShell's description of its approved verbs, your function should be named Invoke-CoutdownTimer, given that it executes synchronously, though the standard Start-Sleep is similarly misnamed - see GitHub docs issue #4474 for a discussion.

Function Start-CountdownTimer {
  param (
    [Parameter(Mandatory)]
    [ValidateRange(0,999)]
    [int] $Seconds,
    [int] $SleepIntervalMilliSeconds = 1000
  )
  $spinner = '|', '/', '-', '\'
  $spinnerPos = 0
  [Console]::CursorVisible = $false
  $dtEnd = [datetime]::UtcNow.AddSeconds($Seconds)
  try {
    while (($remainingSecs = ($dtEnd - [datetime]::UtcNow).TotalSeconds) -gt 0) {
      Write-Host -NoNewline ("`r {0} " -f $spinner[$spinnerPos   % 4]) -ForegroundColor White 
      Write-Host -NoNewLine (" Starting in {0,3}s ...  " -f [Math]::Ceiling($remainingSecs))
      Start-Sleep -Milliseconds ([Math]::Min($SleepIntervalMilliSeconds, $remainingSecs * 1000))
    }
    Write-Host
  } 
  finally {
    [Console]::CursorVisible = $true
  }
}
  • Related