Examples/Invoke-PomodoroDemo.ps1

#Requires -Version 5.1
<#
.SYNOPSIS
    Pomodoro countdown timer demo - showcases the subscription system.

.DESCRIPTION
    A 25-minute countdown timer demonstrating conditional subscriptions:
    the timer subscription is only active when the clock is running,
    so pausing it literally removes the sub from the subscription list.

    Keys:
      Space - toggle start/pause
      R - reset to 25:00
      Q - quit

.NOTES
    Requires the Elm module to be loaded.
    Run from the Examples directory: . .\Invoke-PomodoroDemo.ps1; Invoke-PomodoroDemo
#>

function Invoke-PomodoroDemo {
    [CmdletBinding()]
    param()

    Import-Module "$PSScriptRoot/../Elm.psd1" -Force

    # ------------------------------------------------------------------ Model
    # SecondsLeft : [int] remaining seconds (0..1500)
    # Running : [bool] whether the clock is ticking
    # Phase : [string] 'Work' or 'Break'
    # ------------------------------------------------------------------ Model

    $initFn = {
        [PSCustomObject]@{
            Model = [PSCustomObject]@{
                SecondsLeft = 1500
                Running     = $false
                Phase       = 'Work'
            }
            Cmd = $null
        }
    }

    # ----------------------------------------------------------------- Update
    $updateFn = {
        param($msg, $model)
        switch ($msg) {
            'Tick' {
                $newSecs = $model.SecondsLeft - 1
                if ($newSecs -le 0) {
                    # Switch phases
                    $nextPhase = if ($model.Phase -eq 'Work') { 'Break' } else { 'Work' }
                    $nextSecs  = if ($nextPhase -eq 'Work') { 1500 } else { 300 }
                    $newModel  = [PSCustomObject]@{
                        SecondsLeft = $nextSecs
                        Running     = $false
                        Phase       = $nextPhase
                    }
                    return [PSCustomObject]@{ Model = $newModel; Cmd = $null }
                }
                $newModel = [PSCustomObject]@{
                    SecondsLeft = $newSecs
                    Running     = $model.Running
                    Phase       = $model.Phase
                }
                return [PSCustomObject]@{ Model = $newModel; Cmd = $null }
            }
            'Toggle' {
                $newModel = [PSCustomObject]@{
                    SecondsLeft = $model.SecondsLeft
                    Running     = -not $model.Running
                    Phase       = $model.Phase
                }
                return [PSCustomObject]@{ Model = $newModel; Cmd = $null }
            }
            'Reset' {
                $newModel = [PSCustomObject]@{
                    SecondsLeft = 1500
                    Running     = $false
                    Phase       = 'Work'
                }
                return [PSCustomObject]@{ Model = $newModel; Cmd = $null }
            }
            'Quit' {
                return [PSCustomObject]@{
                    Model = $model
                    Cmd   = [PSCustomObject]@{ Type = 'Quit' }
                }
            }
            default {
                return [PSCustomObject]@{ Model = $model; Cmd = $null }
            }
        }
    }

    # ------------------------------------------------------------------- View
    $viewFn = {
        param($model)

        $mins  = [int][math]::Floor($model.SecondsLeft / 60)
        $secs  = $model.SecondsLeft % 60
        $clock = '{0:D2}:{1:D2}' -f $mins, $secs

        $phaseLabel  = if ($model.Phase -eq 'Work') { 'Work' } else { 'Break' }
        $stateLabel  = if ($model.Running) { 'running' } else { 'paused' }
        $toggleLabel = if ($model.Running) { '[Space] Pause' } else { '[Space] Start' }

        # Progress bar (40 chars wide)
        $total      = if ($model.Phase -eq 'Work') { 1500 } else { 300 }
        $filled     = [int][math]::Floor(($total - $model.SecondsLeft) / $total * 40)
        $empty      = 40 - $filled
        $bar        = ('[') + ([string]'#' * $filled) + ([string]'-' * $empty) + (']')

        $titleStyle  = New-ElmStyle -Foreground 'Cyan'    -Bold
        $clockStyle  = New-ElmStyle -Foreground 'White'   -Bold
        $phaseStyle  = New-ElmStyle -Foreground $(if ($model.Phase -eq 'Work') { 'Red' } else { 'Green' })
        $barStyle    = New-ElmStyle -Foreground 'BrightBlack'
        $hintStyle   = New-ElmStyle -Foreground 'BrightBlack'
        $stateStyle  = New-ElmStyle -Foreground $(if ($model.Running) { 'Green' } else { 'Yellow' })

        New-ElmBox -Children @(
            New-ElmText -Content ' Pomodoro Timer ' -Style $titleStyle
            New-ElmText -Content ''
            New-ElmText -Content " $phaseLabel " -Style $phaseStyle
            New-ElmText -Content " $clock " -Style $clockStyle
            New-ElmText -Content " $bar " -Style $barStyle
            New-ElmText -Content " $stateLabel " -Style $stateStyle
            New-ElmText -Content ''
            New-ElmText -Content " $toggleLabel [R] Reset [Q] Quit " -Style $hintStyle
        )
    }

    # ---------------------------------------------------------- Subscriptions
    # The timer sub is only active when Running is true - pausing removes it.
    $subFn = {
        param($model)
        $subs = [System.Collections.Generic.List[object]]::new()
        $subs.Add((New-ElmKeySub -Key 'Q'     -Handler { 'Quit'   }))
        $subs.Add((New-ElmKeySub -Key 'Space' -Handler { 'Toggle' }))
        $subs.Add((New-ElmKeySub -Key 'R'     -Handler { 'Reset'  }))
        if ($model.Running) {
            $subs.Add((New-ElmTimerSub -IntervalMs 1000 -Handler { 'Tick' }))
        }
        return $subs.ToArray()
    }

    Start-ElmProgram -InitFn $initFn -UpdateFn $updateFn -ViewFn $viewFn -SubscriptionFn $subFn
}

Invoke-PomodoroDemo