The Pomodoro Technique is a popular time management method developed by Francesco Cirillo in the late 1980s. The concept is simple: you break your work into 25-minute intervals (called “pomodoros”), separated by short 5-minute breaks. After every four cycles, you take a longer break. This approach helps maintain focus, prevents burnout, and provides better control over your workflow.
In this article, I will show you how to write your own Pomodoro timer using pure PowerShell. It will run in the background, display a countdown in the window title, and be controlled via a system tray context menu—all while keeping the script under 100 lines of code.
We will leverage the .NET Framework (WinForms libraries), which allows us to:
-
Create an icon in the notification area (system tray).
-
Implement a context menu.
-
Use a “Timer” object for background code execution.
Step 1: Preparing Libraries and Variables
To create the graphical interface, we need the System.Windows.Forms and System.Drawing assemblies. We will also define the time settings and use the $script: scope to ensure that event handlers (menu clicks) have direct access to the timer variables.
Add-Type -AssemblyName System.Windows.Forms, System.Drawing
# Time settings (in minutes)
$workTime = 25
$breakTime = 5
$script:timeLeft = $workTime * 60
$script:isWorking = $true
Step 2: Creating the Tray Icon (NotifyIcon)
The NotifyIcon object is responsible for the icon appearing in the system tray (next to the clock). For maximum stability in Windows 10/11, we will use the standard “Information” system icon (a blue circle with an “i”).
$script:ni = New-Object System.Windows.Forms.NotifyIcon
$script:ni.Icon = [System.Drawing.SystemIcons]::Information
$script:ni.Visible = $true
$script:ni.Text = "Pomodoro: Right-click to start"
Step 3: Timer Logic and Phase Automation
The timer functions as an event that triggers every second. This logic includes an automatic transition: when the work time reaches zero, the script automatically switches to break mode and begins a new countdown.
Important Note: In modern PowerShell versions (7+), you must explicitly cast numbers to [int] when formatting strings to avoid processing errors.
$script:timer = New-Object System.Windows.Forms.Timer
$script:timer.Interval = 1000
$script:timer.Add_Tick({
if ($script:timeLeft -gt 0) {
$script:timeLeft--
} else {
# Switch mode Work <-> Break
$script:isWorking = -not $script:isWorking
$script:timeLeft = ($script:isWorking ? $workTime : $breakTime) * 60
# Windows balloon notification
$msg = if ($script:isWorking) { "Time to work!" } else { "Time for a break!" }
$script:ni.ShowBalloonTip(5000, "Pomodoro", $msg, "Info")
}
# Visual Updates (Tray, Window Title, Console)
[int]$m = [Math]::Floor($script:timeLeft / 60)
[int]$s = $script:timeLeft % 60
$phase = if ($script:isWorking) { "WORK" } else { "BREAK" }
$displayTime = "{0:D2}:{1:D2}" -f $m, $s
$script:ni.Text = "Pomodoro [$phase]: $displayTime"
$host.UI.RawUI.WindowTitle = "[$displayTime] - $phase"
Write-Host "`r>>> Phase: $phase | Time left: $displayTime " -NoNewline -ForegroundColor Cyan
})
Step 4: Context Menu Management
To ensure that menu buttons trigger reliably in any PowerShell version, we create them as individual ToolStripMenuItem objects and subscribe to the Click event.
$menu = New-Object System.Windows.Forms.ContextMenuStrip
$btnStart = New-Object System.Windows.Forms.ToolStripMenuItem("Start / Pause")
$btnStart.add_Click({
if ($script:timer.Enabled) { $script:timer.Stop() } else { $script:timer.Start() }
})
$btnExit = New-Object System.Windows.Forms.ToolStripMenuItem("Exit")
$btnExit.add_Click({
$script:ni.Visible = $false
[System.Windows.Forms.Application]::Exit()
Stop-Process -Id $PID
})
[void]$menu.Items.Add($btnStart)
[void]$menu.Items.Add("-") # Separator
[void]$menu.Items.Add($btnExit)
$script:ni.ContextMenuStrip = $menu
Step 5: Starting the Event Loop
Without this method, the script would terminate immediately after execution. The Run() command forces the application to “wait” for user actions and timer signals.
[System.Windows.Forms.Application]::Run()
The final full code
Add-Type -AssemblyName System.Windows.Forms, System.Drawing
# Time settings (in minutes)
$workTime = 25
$breakTime = 5
$script:timeLeft = $workTime * 60
$script:isWorking = $true
# Create objects
$script:ni = New-Object System.Windows.Forms.NotifyIcon
$script:timer = New-Object System.Windows.Forms.Timer
$script:timer.Interval = 1000
# MAIN TIMER LOGIC
$script:timer.Add_Tick({
if ($script:timeLeft -gt 0) {
$script:timeLeft--
} else {
# When time is up: switch mode (Work <-> Break)
$script:isWorking = -not $script:isWorking
$script:timeLeft = ($script:isWorking ? $workTime : $breakTime) * 60
# Notification
$status = if ($script:isWorking) { "Work" } else { "Break" }
$msg = if ($script:isWorking) { "Time to get to work!" } else { "Time for a break!" }
$script:ni.ShowBalloonTip(5000, "Pomodoro: $status", $msg, "Info")
Write-Host "`n[!] Phase change: $msg" -ForegroundColor Yellow
}
# UI UPDATE (every second)
[int]$m = [Math]::Floor($script:timeLeft / 60)
[int]$s = $script:timeLeft % 60
$phase = if ($script:isWorking) { "WORK" } else { "BREAK" }
$displayTime = "{0:D2}:{1:D2}" -f $m, $s
# Tray icon tooltip text
$script:ni.Text = "Pomodoro [$phase]: $displayTime"
# Window title
$host.UI.RawUI.WindowTitle = "[$displayTime] - $phase"
# Console output
$color = if ($script:isWorking) { "Cyan" } else { "Green" }
Write-Host "`r>>> Current phase: $phase | Time left: $displayTime " -NoNewline -ForegroundColor $color
})
# Icon Setup
$script:ni.Icon = [System.Drawing.SystemIcons]::Information
$script:ni.Visible = $true
# Context Menu
$menu = New-Object System.Windows.Forms.ContextMenuStrip
$btnStart = New-Object System.Windows.Forms.ToolStripMenuItem("Start / Pause")
$btnReset = New-Object System.Windows.Forms.ToolStripMenuItem("Reset to Work Start")
$btnExit = New-Object System.Windows.Forms.ToolStripMenuItem("Exit")
$btnStart.add_Click({
if ($script:timer.Enabled) {
$script:timer.Stop()
Write-Host "`n>>> PAUSED" -ForegroundColor Yellow
} else {
$script:timer.Start()
Write-Host "`n>>> TIMER STARTED" -ForegroundColor Green
}
})
$btnReset.add_Click({
$script:timer.Stop()
$script:isWorking = $true
$script:timeLeft = $workTime * 60
Write-Host "`n>>> RESET TO WORK START" -ForegroundColor Gray
})
$btnExit.add_Click({
$script:ni.Visible = $false
$script:timer.Stop()
[System.Windows.Forms.Application]::Exit()
Stop-Process -Id $PID
})
[void]$menu.Items.Add($btnStart)
[void]$menu.Items.Add($btnReset)
[void]$menu.Items.Add("-") # Separator
[void]$menu.Items.Add($btnExit)
$script:ni.ContextMenuStrip = $menu
Write-Host ">>> Pomodoro started." -ForegroundColor White
Write-Host ">>> Work duration: $workTime min, Break duration: $breakTime min." -ForegroundColor Gray
Write-Host ">>> RIGHT-CLICK the 'i' icon in the tray -> Start." -ForegroundColor Cyan
# Start the event loop
[System.Windows.Forms.Application]::Run()