StartDemo.psm1

## Start-Demo.ps1
##################################################################################################
## This is an overhaul of Jeffrey Snover's original Start-Demo script by Joel "Jaykul" Bennett
## Terms:
## http://poshcode.org/Terms.html
##
## I've switched it to using ReadKey instead of ReadLine (you don't have to hit Enter each time)
## As a result, I've changed the names and keys for a lot of the operations, so that they make
## sense with only a single letter to tell them apart (sorry if you had them memorized).
##
## I've also been adding features as I come across needs for them, and you'll contribute your
## improvements back to the PowerShell Script repository as well.
##################################################################################################
## Revision History (version 3.3)
## 3.3.3 Fixed: Script no longer says "unrecognized key" when you hit shift or ctrl, etc.
## Fixed: Blank lines in script were showing as errors (now printed like comments)
## 3.3.2 Fixed: Changed the "x" to match the "a" in the help text
## 3.3.1 Fixed: Added a missing bracket in the script
## 3.3 - Added: Added a "Clear Screen" option
## - Added: Added a "Rewind" function (which I'm not using much)
## 3.2 - Fixed: Put back the trap { continue; }
## 3.1 - Fixed: No Output when invoking Get-Member (and other cmdlets like it???)
## 3.0 - Fixed: Commands which set a variable, like: $files = ls
## - Fixed: Default action doesn't continue
## - Changed: Use ReadKey instead of ReadLine
## - Changed: Modified the option prompts (sorry if you had them memorized)
## - Changed: Various time and duration strings have better formatting
## - Enhance: Colors are settable: prompt, command, comment
## - Added: NoPauseAfterExecute switch removes the extra pause
## If you set this, the next command will be displayed immediately
## - Added: Auto Execute mode (FullAuto switch) runs the rest of the script
## at an automatic speed set by the AutoSpeed parameter (or manually)
## - Added: Automatically append an empty line to the end of the demo script
## so you have a chance to "go back" after the last line of you demo
##################################################################################################
##
Function Start-Demo
{
param(
  $file='.\demo.ps1', 
  [int]$command=0, 
  [System.ConsoleColor]$promptColor='Yellow', 
  [System.ConsoleColor]$commandColor='White', 
  [System.ConsoleColor]$commentColor='Green', 
  [switch]$FullAuto,
  [int]$AutoSpeed = 3,
  [switch]$PauseAfterExecute,
  [switch]$ShowProgress
)
if($ShowProgress)
{
    $Script:ProgressPreference = 'Continue'
}
else {
   $Script:ProgressPreference = 'SilentlyContinue' 
}
$RawUI = $Host.UI.RawUI
$hostWidth = $RawUI.BufferSize.Width

# A function for reading in a character
function Read-Char() {
  $_OldColor = $RawUI.ForeGroundColor
  try{
    $RawUI.ForeGroundColor = 'Red'
    $inChar=$RawUI.ReadKey('IncludeKeyUp')
    # loop until they press a character, so Shift or Ctrl, etc don't terminate us
    while($inChar.Character -eq 0){
        $inChar=$RawUI.ReadKey('IncludeKeyUp')
    }
  }
  finally
  {
    $RawUI.ForeGroundColor = $_OldColor
  }
  return $inChar.Character
}

function Rewind($lines, $index, $steps = 1) {
   $started = $index;
   $index -= $steps;
   while(($index -ge 0) -and ($lines[$index].Trim(" `t").StartsWith('#'))){
      $index--
   }
   if( $index -lt 0 ) { $index = $started }
   return $index
}

$file = Resolve-Path $file
while(-not(Test-Path $file)) {
  $file = Read-Host 'Please enter the path of your demo script (Crtl+C to cancel)'
  $file = Resolve-Path $file
}

Clear-Host

$_lines = Get-Content $file
# Append an extra (do nothing) line on the end so we can still go back after the last line.
$_lines += "Write-Host 'The End'"
$_starttime = [DateTime]::now
$FullAuto = $false

Write-Host -nonew -back black -fore $promptColor $(' ' * $hostWidth)
Write-Host -nonew -back black -fore $promptColor @"
<Demo Started :: $(split-path $file -leaf)>$(' ' * ($hostWidth -(18 + $(split-path $file -leaf).Length)))
"@

Write-Host -nonew -back black -fore $promptColor 'Press'
Write-Host -nonew -back black -fore Red ' ? '
Write-Host -nonew -back black -fore $promptColor "for help.$(' ' * ($hostWidth -17))"
Write-Host -nonew -back black -fore $promptColor $(' ' * $hostWidth)

# We use a FOR and an INDEX ($_i) instead of a FOREACH because
# it is possible to start at a different location and/or jump
# around in the order.
for ($_i = $Command; $_i -lt $_lines.count; $_i++)
{  
    # Put the current command in the Window Title along with the demo duration
    $Dur = [DateTime]::Now - $_StartTime
   $RawUI.WindowTitle = "$(if($dur.Hours -gt 0){'{0}h '})$(if($dur.Minutes -gt 0){'{1}m '}){2}s {3}" -f 
                        $dur.Hours, $dur.Minutes, $dur.Seconds, $($_Lines[$_i])

    # Echo out the commmand to the console with a prompt as though it were real
    Write-Host -nonew -fore $promptColor "[$_i]$([char]0x2265) "
    if ($_lines[$_i].Trim(' ').StartsWith('#') -or $_lines[$_i].Trim(' ').Length -le 0) { 
        Write-Host -fore $commentColor "$($_Lines[$_i]) "
        continue 
    } else {
        Write-Host -nonew -fore $commandColor "$($_Lines[$_i]) "
    }

    if( $FullAuto ) { Start-Sleep $autoSpeed; $ch = [char]13 } else { $ch = Read-Char }
    switch($ch)
    {
        '?' {
            Write-Host -Fore $promptColor @"
 
Running demo: $file
(n) Next (p) Previous
(q) Quit (s) Suspend
(t) Timecheck (v) View $(split-path $file -leaf)
(g) Go to line by number
(f) Find lines by string
(a) Auto Execute mode
(c) Clear Screen
"@

            $_i-- # back a line, we're gonna step forward when we loop
        }
        'n' { # Next (do nothing)
            Write-Host -Fore $promptColor '<Skipping Line>'
        }
        'p' { # Previous
            Write-Host -Fore $promptColor '<Back one Line>'
            while ($_lines[--$_i].Trim(' ').StartsWith('#')){}
            $_i-- # back a line, we're gonna step forward when we loop
        }
        'a' { # EXECUTE (Go Faster)
            $AutoSpeed = [int](Read-Host 'Pause (seconds)')
            $FullAuto = $true;
            Write-Host -Fore $promptColor '<eXecute Remaining Lines>'
            $_i-- # Repeat this line, and then just blow through the rest
        }
        'q' { # Quit
            Write-Host -Fore $promptColor '<Quiting demo>'
            $_i = $_lines.count;
            break;
        }
        'v' { # View Source
            $lines[0..($_i-1)] | Write-Host -Fore Yellow 
            $lines[$_i]        | Write-Host -Fore Green
            $lines[($_i+1)..$lines.Count] | Write-Host -Fore Yellow 
            $_i-- # back a line, we're gonna step forward when we loop
        }
        't' { # Time Check
             $dur = [DateTime]::Now - $_StartTime
       Write-Host -Fore $promptColor $(
          "{3} -- $(if($dur.Hours -gt 0){'{0}h '})$(if($dur.Minutes -gt 0){'{1}m '}){2}s" -f 
          $dur.Hours, $dur.Minutes, $dur.Seconds, ([DateTime]::Now.ToShortTimeString()))
             $_i-- # back a line, we're gonna step forward when we loop
        }
        's' { # Suspend (Enter Nested Prompt)
            Write-Host -Fore $promptColor "<Suspending demo - type 'Exit' to resume>"
            $Host.EnterNestedPrompt()
            $_i-- # back a line, we're gonna step forward when we loop
        }
        'g' { # GoTo Line Number
            $i = [int](Read-Host 'line number')
            if($i -le $_lines.Count) {
                if($i -gt 0) {
               # extra line back because we're gonna step forward when we loop
               $_i = Rewind $_lines $_i (($_i-$i)+1)
                } else {
                    $_i = -1 # Start negative, because we step forward when we loop
                }
            }
        }
        'f' { # Find by pattern
            $match = $_lines | Select-String (Read-Host 'search string')
            if($match -eq $null) {
                Write-Host -Fore Red "Can't find a matching line"
            } else {
                $match | % { Write-Host -Fore $promptColor $('[{0,2}] {1}' -f ($_.LineNumber - 1), $_.Line) }
                if($match.Count -lt 1) {
                    $_i = $match.lineNumber - 2  # back a line, we're gonna step forward when we loop
                } else {               
                    $_i-- # back a line, we're gonna step forward when we loop
                }
            }
        }
      'c' { 
         Clear-Host
         $_i-- # back a line, we're gonna step forward when we loop
      }
        "$([char]13)" { # on enter
            Write-Host
            trap [System.Exception] {Write-Error $_; continue;}
            Invoke-Expression ($_lines[$_i]) | out-default
            if( $PauseAfterExecute -and -not $FullAuto) { 
                $null = $RawUI.ReadKey('NoEcho,IncludeKeyUp')  # Pause after output for no apparent reason... ;)
            }
        }
        default
        {
            Write-Host -Fore Green "`nKey not recognized. Press ? for help, or ENTER to execute the command."
            $_i-- # back a line, we're gonna step forward when we loop
        }
    }
}
$dur = [DateTime]::Now - $_StartTime
Write-Host -Fore $promptColor $(
   "<Demo Complete -- $(if($dur.Hours -gt 0){'{0}h '})$(if($dur.Minutes -gt 0){'{1}m '}){2}s>" -f 
   $dur.Hours, $dur.Minutes, $dur.Seconds, [DateTime]::Now.ToLongTimeString())
Write-Host -Fore $promptColor $([DateTime]::now)
Write-Host
}