PomoShell.psm1
# # Script module for module 'PomoShell' # #Requires -PSEdition Core #Requires -Version 7.0 #Requires -Module BurntToast enum PhaseStatus { New Running Paused Skipped Aborted } class Phase { [string] $Name [uint] $Duration # In minute [uint] $Turn hidden [PhaseStatus] $Status hidden [datetime] $StartDate hidden [datetime] $EndDate hidden [datetime] $PauseDate hidden [uint] $SecondsRemainingAtPause Phase( [string] $Name, [uint] $Duration, [uint] $Turn ) { $this.Name = $Name $this.Duration = $Duration $this.Turn = $Turn $this.Status = [PhaseStatus]::"New" } [bool] IsNew() { return $this.Status -eq [PhaseStatus]::"New" } [bool] IsRunning() { return $this.Status -eq [PhaseStatus]::"Running" } [bool] IsPaused() { return $this.Status -eq [PhaseStatus]::"Paused" } [bool] IsSkippedOrAborted() { return $this.Status -in ([PhaseStatus]::"Skipped", [PhaseStatus]::"Aborted") } [bool] IsCompleted() { if ($this.IsRunning()) { return (Get-Date) -gt $this.EndDate } elseif ($this.IsPaused()) { return $this.SecondsRemainingAtPause -eq 0 } return $this.IsSkippedOrAborted() } [void] Start() { if ($this.IsNew()) { $this.StartDate = Get-Date $this.EndDate = $this.StartDate.AddMinutes($this.Duration) $this.Status = [PhaseStatus]::"Running" Write-Debug -Message ("[Phase] {0} started (Turn {1})." -f $this.Name, $this.Turn) } } [void] Pause() { if ($this.IsRunning()) { $this.PauseDate = Get-Date $this.SecondsRemainingAtPause = $this.GetSecondsRemaining() $this.Status = [PhaseStatus]::"Paused" Write-Debug -Message ("[Phase] {0} paused." -f $this.Name) } } [void] Resume() { if ($this.IsPaused()) { $this.EndDate = (Get-Date).AddSeconds($this.SecondsRemainingAtPause) $this.Status = [PhaseStatus]::"Running" Write-Debug -Message ("[Phase] {0} resumed." -f $this.Name) } } [void] Skip() { if (-not $this.IsSkippedOrAborted()) { $this.EndDate = Get-Date $this.Status = [PhaseStatus]::"Skipped" Write-Debug -Message ("[Phase] {0} skipped." -f $this.Name) } } [void] Abort() { if (-not $this.IsSkippedOrAborted()) { $this.EndDate = Get-Date $this.Status = [PhaseStatus]::"Aborted" Write-Debug -Message ("[Phase] {0} aborted." -f $this.Name) } } [double] GetSecondsRemaining() { if ($this.IsPaused()) { return $this.SecondsRemainingAtPause } return ($this.EndDate - (Get-Date)).TotalSeconds } [int] GetPercentComplete() { return 100 - (($this.GetSecondsRemaining() / ($this.Duration * 60)) * 100) } [string] GetActivityName() { return "{0} - Turn {1}" -f $this.Name, $this.Turn } [string] GetStatusDescription() { if ($this.IsRunning()) { return "{0} (ends at {1})" -f $this.Status, $this.EndDate.ToShortTimeString() } elseif ($this.IsPaused()) { return "{0} (at {1})" -f $this.Status, $this.PauseDate.ToShortTimeString() } elseif ($this.IsSkippedOrAborted()) { return "{0} (at {1})" -f $this.Status, $this.EndDate.ToShortTimeString() } return $this.Status } [hashtable] GetEndStats() { if ($this.IsCompleted()) { return @{ StartDate = $this.StartDate EndDate = $this.EndDate TotalMinutes = [Math]::Floor(($this.EndDate - $this.StartDate).TotalMinutes) } } return $null } [string] ToString() { return "{0}, {1} minutes, turn {2}" -f $this.Name, $this.Duration, $this.Turn } } function Invoke-Toast { param( [Parameter(Mandatory = $true)] [string] $Text ) begin { $BurntToastNotification = @{ Text = "PomoShell", $Text AppLogo = Join-Path -Path $PSScriptRoot -ChildPath "PomoShell.png" Silent = $true Confirm = $false } } process { try { New-BurntToastNotification @BurntToastNotification Write-Debug -Message ("[Toast] Shows `"{0}`"." -f $Text) } catch { Write-Error -Message ("[Toast] Cannot show `"{0}`": {1}" -f $Text, $_.Exception.Message) } } } function Invoke-Speech { param( [Parameter(Mandatory = $true)] [string] $Text ) begin { <# SpeechVoiceSpeakFlags o SVSFDefault = 0 # Sync o SVSFlagsAsync = 1 # Async (buggy) #> $SPVoiceFlag = 0 } process { try { $SPVoice = New-Object -ComObject SAPI.SPVoice $EnglishVoice = $SPVoice.GetVoices() | Where-Object -Property Id -Match ".*\\TTS_MS_EN-.*" | Select-Object -First 1 if ($EnglishVoice) { $SPVoice.Voice = $EnglishVoice } else { Write-Warning -Message ("[Speech] No English voice found.") } $SPVoice.Speak($Text, $SPVoiceFlag) | Out-Null Write-Debug -Message ("[Speech] Says `"{0}`" with `"{1}`"." -f $Text, $SPVoice.Voice.GetDescription()) } catch { Write-Error -Message ("[Speech] Cannot say `"{0}`": {1}" -f $Text, $_.Exception.Message) } } } function Push-Notification { param( [Parameter(Mandatory = $true)] [string] $Text, [Parameter()] [bool] $NoToast = $false, [Parameter()] [bool] $NoSpeech = $false ) process { Write-Information -MessageData $Text if (-not $NoToast) { Invoke-Toast -Text $Text } if (-not $NoSpeech) { Invoke-Speech -Text $Text } } } function Invoke-Pomodoro { <# .SYNOPSIS Invokes pomodoro. .DESCRIPTION Invokes a pomodoro in your Powershell console. .PARAMETER FocusDuration Duration of a focus time in minute. .PARAMETER ShortBreakDuration Duration of a short break time in minute. .PARAMETER LongBreakDuration Duration of a long break time in minute. .PARAMETER LongBreakInterval Interval when a long break is triggered. .PARAMETER NoToastNotification Interval when a long break is triggered. .PARAMETER NoVoiceNotification Interval when a long break is triggered. .INPUTS None. You cannot pipe objects to Invoke-Pomodoro. .OUTPUTS PSCustomObject. The detailed phases that were done during the execution of the pomodoro. .EXAMPLE PS> Invoke-Pomodoro Start pomodoro with the default durations. .EXAMPLE PS> Invoke-Pomodoro -Focus 15 -ShortBreak 3 -LongBreak 10 -Interval 3 Start pomodoro with custom durations. .EXAMPLE PS> Invoke-Pomodoro -NoToast -NoVoice Start pomodoro with all notifications turned off. .LINK GitHub repository: https://github.com/VouDoo/PomoShell .NOTES Key bindings: o <Space>: Pause or resume the current phase. o <S>: Skip the current phase. o <Q>: Stop the pomodoro. #> [CmdletBinding()] param( [Parameter(HelpMessage = "Duration of a focus time in minute")] [Alias("Focus")] [uint] $FocusDuration = 25, [Parameter(HelpMessage = "Duration of a short break time in minute")] [Alias("ShortBreak")] [uint] $ShortBreakDuration = 5, [Parameter(HelpMessage = "Duration of a long break time in minute")] [Alias("LongBreak")] [uint] $LongBreakDuration = 15, [Parameter(HelpMessage = "Interval when a long break is triggered")] [Alias("Interval")] [uint] $LongBreakInterval = 4, [Parameter(HelpMessage = "No Windows Toast notification will be shown")] [Alias("NoToast")] [switch] $NoToastNotification, [Parameter(HelpMessage = "No voice notification will be triggered")] [Alias("NoVoice")] [switch] $NoVoiceNotification ) begin { $Continue = $true $Turn = 1 $BreakPhase = $false $CompletedPhases = @() $NotificationOptions = @{ NoToast = $NoToastNotification.IsPresent NoSpeech = $NoVoiceNotification.IsPresent } } process { Write-Debug -Message "[Pomo] STARTED." #region Main loop while ($Continue) { # Create Phase object if ($BreakPhase) { if (($Turn % $LongBreakInterval) -eq 0) { $Phase = New-Object -TypeName Phase -ArgumentList "Long Break", $LongBreakDuration, $Turn } else { $Phase = New-Object -TypeName Phase -ArgumentList "Short Break", $ShortBreakDuration, $Turn } $BreakPhase = $false $Turn++ } else { $Phase = New-Object -TypeName Phase -ArgumentList "Focus", $FocusDuration, $Turn $BreakPhase = $true } Write-Debug -Message ("[Pomo] Phase initiated: {0}." -f $Phase.ToString()) $Phase.Start() Push-Notification @NotificationOptions -Text ("{0} has started." -f $Phase.Name) $Host.UI.RawUI.FlushInputBuffer() #region Phase Loop while (-not $Phase.IsCompleted() -and $Continue) { # Key actions if ($Host.UI.RawUI.KeyAvailable) { $Key = $Host.UI.RawUI.ReadKey("NoEcho, IncludeKeyDown") Write-Debug -Message "[ReadKey] $Key." switch ($Key.VirtualKeyCode) { # [Space] Pause/Resume the current phase 32 { if ($Phase.IsRunning()) { $Phase.Pause() } elseif ($Phase.IsPaused()) { $Phase.Resume() } } # [S] Skip the current phase 83 { $Phase.Skip() } # [Q] Stop the pomodoro 81 { $Phase.Pause() $IsAnswered = $false while (-not $IsAnswered) { switch ((Read-Host -Prompt "Do you want to stop the pomodoro? [Y/N]").Trim().ToLower()) { "y" { $IsAnswered = $true $Continue = $false $Phase.Abort() Write-Debug -Message "[Pomo] Stopping..." } "n" { $IsAnswered = $true $Phase.Resume() } } } } } $Host.UI.RawUI.FlushInputBuffer() } $Progress = @{ Activity = $Phase.GetActivityName() Status = $Phase.GetStatusDescription() PercentComplete = $Phase.GetPercentComplete() SecondsRemaining = $Phase.GetSecondsRemaining() } Write-Progress @Progress Start-Sleep -Milliseconds 666 } #endregion Phase Loop Write-Progress -Activity $Phase.GetActivityName() -Completed Push-Notification @NotificationOptions -Text ("{0} has ended." -f $Phase.Name) $PhaseEndStats = $Phase.GetEndStats() $CompletedPhases += [PSCustomObject] @{ Phase = $Phase.Name Turn = $Phase.Turn Start = $PhaseEndStats.StartDate End = $PhaseEndStats.EndDate TotalMinutes = $PhaseEndStats.TotalMinutes } Write-Debug -Message ("[Pomo] Phase completed: {0}." -f $Phase.ToString()) Start-Sleep -Seconds 1 } #endregion Main loop Write-Debug -Message "[Pomo] STOPPED." } end { Write-Debug -Message "[Pomo] Returns completed phases." $CompletedPhases } } # Create alias(es) New-Alias -Name Pomo -Value "Invoke-Pomodoro" # export member(s) Export-ModuleMember -Function Invoke-Pomodoro -Alias Pomo |