public/Invoke-PwshTyper.ps1

function Invoke-PwshTyper {
  [CmdletBinding()]
  param (
    # The mode to run e.g. Pinky, Ring, Middle, Index, or one of the wordlists in ../wordlists
    [string] $Mode,
    # The duration in seconds to run the test for
    [int] $Duration,
    # Whether to include punctuation in the test e.g. country name! and city name? (only applies to wordlists)
    [bool] $Punctuation,
    # Whether to use title case in the test e.g. Country Name and City Name (only applies to wordlists)
    [bool] $TitleCase
  )
  
  $ErrorActionPreference = "Stop"

  # Read the files in $PSScriptRoot/../wordlists
  $wordModes = Get-ChildItem -Path "$PSScriptRoot/../wordlists" -Filter "*.txt" -Recurse | ForEach-Object {
    $firstChar = $_.BaseName[0].ToString().ToUpper()
    $endOfWord = $_.BaseName.Substring(1)
    return "$firstChar$endOfWord"
  }

  # Read the default settings file
  $settingsFile = "$PSScriptRoot/../settings.json"
  if (Test-Path -Path $settingsFile) {
    $settings = Get-Content -Path $settingsFile | ConvertFrom-Json
  } else {
    $settings = @{
      Punctuation = "y"
      TitleCase = "y"
      Duration = 60
    }
  }

  # Setup
  $choices = @{
    "Word Modes" = @($wordModes)
    "Finger Modes" = @(
      "Pinky",
      "Ring",
      "Middle",
      "Index"
    )
  }

  if (!$PSBoundParameters.ContainsKey("Mode")) {
    $Mode = Read-SpectreSelectionGrouped -Message "Select a paragraph generator mode..." -PageSize 100 -Choices $choices
  }
  Write-SpectreHost "[grey42]Mode:[/] $Mode"

  if (!$PSBoundParameters.ContainsKey("Punctuation")) {
    $Punctuation = Read-SpectreConfirm -Message "Do you want to include punctuation?" -DefaultAnswer $settings.Punctuation
    if ($Punctuation) {
      $settings.Punctuation = "y"
    } else {
      $settings.Punctuation = "n"
    }
  }
  Write-SpectreHost "[grey42]Punctuation:[/] $Punctuation"

  if (!$PSBoundParameters.ContainsKey("TitleCase")) {
    $TitleCase = Read-SpectreConfirm -Message "Do you want to use title case?" -DefaultAnswer $settings.TitleCase
  if ($TitleCase) {
      $settings.TitleCase = "y"
    } else {
      $settings.TitleCase = "n"
    }
  }
  Write-SpectreHost "[grey42]TitleCase:[/] $TitleCase"

  if (!$PSBoundParameters.ContainsKey("Duration")) {
    $settings.Duration = Read-SpectreText -Prompt "Enter the duration in seconds: " -DefaultAnswer $settings.Duration
    $Duration = $settings.Duration
  }
  Write-SpectreHost "[grey42]Duration:[/] $Duration seconds"

  # Update the default settings file with the new values
  $settingsJson = $settings | ConvertTo-Json -Depth 5
  $settingsJson | Out-File -FilePath $settingsFile -Force

  # Layout
  $layout = New-SpectreLayout -Name "root" -Rows @(
    # Row 1
    (New-SpectreLayout -Name "title" -MinimumSize 8 -Ratio 1 -Data (Format-TitleComponent)),
    # Row 2
    (New-SpectreLayout -Name "main" -Ratio 100 -Data ("empty")),
    # Row 3
    (New-SpectreLayout -Name "results" -Ratio 200 -Data ("empty"))
  )
  $layout["results"].IsVisible = $false

  # Main loop
  Invoke-SpectreLive -Data $layout -ScriptBlock {
    param (
      [Spectre.Console.LiveDisplayContext] $Context
    )

    # Data
    $start = $null
    $current = ""
    $mistakes = 0
    $allowedKeys = $script:KeyMappings.Pinky + $script:KeyMappings.Ring + $script:KeyMappings.Middle + $script:KeyMappings.Index + $script:KeyMappings.Thumb
    $sentence = switch ($Mode) {
      "Pinky" { Get-PinkyOnlySentence }
      "Ring" { Get-RingFingerOnlySentence }
      "Middle" { Get-MiddleFingerOnlySentence }
      "Index" { Get-IndexFingerOnlySentence }
      default { Get-RandomWordlist -Path "$PSScriptRoot/../wordlists/$Mode.txt" -Punctuation $Punctuation -TitleCase $TitleCase -AllowedKeys $allowedKeys }
    }

    while ($true) {
      # Render TUI components
      Format-TextEntryComponent -Context $Context -RootLayout $layout -TargetLayoutName "main" -Sentence $sentence -CurrentInput $current -Timeout $Duration -Start $start

      # Handle input
      $lastKeysPressed = Get-LastKeysPressed

      foreach ($key in $lastKeysPressed) {
        if ($key.Key -eq "C" -and $key.Modifiers -eq "Control") {
          return
        } elseif ($key.Key -eq "Backspace" -and $current.Length -gt 0) {
          # Remove the last character from the current string
          $current = $current.Substring(0, $current.Length - 1)
        } elseif ($allowedKeys -notcontains $key.KeyChar) {
          # Ignore some keys
        } else {
          # Start the timer on the first key press
          if ($null -eq $start) {
            $start = Get-Date
          }
    
          # Add the last key pressed to the current string
          $current += $key.KeyChar
          
          # Check for mistakes
          if (Test-LastCharMismatch -CurrentInput $current -Sentence $sentence) {
            $mistakes++
          }
        }
      }

      if ((Get-TimeRemaining -Start $start -Timeout $Duration) -lt 0) {
        break
      }
    }

    # Render the results component
    Format-ResultsComponent -Context $Context -Layout $layout["results"] -Mistakes $mistakes -Completed $current -Mode $Mode -Timeout $Duration
  }
}