Windowsウィジェット/タスク管理
ウィジェット
TaskWidget.ps1
<#
============================================================
TaskWidget.ps1 - Desktop Task Widget (Windows Native)
============================================================
#>
Add-Type -AssemblyName PresentationFramework
Add-Type -AssemblyName PresentationCore
Add-Type -AssemblyName WindowsBase
Add-Type -AssemblyName System.Windows.Forms
# --- Data ---
$script:DataDir = Join-Path $env:USERPROFILE ".taskwidget"
$script:DataFile = Join-Path $script:DataDir "tasks.json"
if (-not (Test-Path $script:DataDir)) {
New-Item -ItemType Directory -Path $script:DataDir -Force | Out-Null
}
function Load-Tasks {
if (Test-Path $script:DataFile) {
try {
$json = Get-Content $script:DataFile -Raw -Encoding UTF8
$tasks = $json | ConvertFrom-Json
if ($tasks -isnot [Array]) { $tasks = @($tasks) }
return $tasks
} catch { return @() }
}
return @(
[PSCustomObject]@{ Id=[guid]::NewGuid().ToString(); Text=([char]0x30BF)+([char]0x30B9)+([char]0x30AF)+([char]0x3092)+([char]0x8FFD)+([char]0x52A0)+([char]0x3057)+([char]0x3066)+([char]0x307F)+([char]0x3088)+([char]0x3046); Done=$false; DueDate="" },
[PSCustomObject]@{ Id=[guid]::NewGuid().ToString(); Text=([char]0x671F)+([char]0x9650)+([char]0x4ED8)+([char]0x304D)+([char]0x30BF)+([char]0x30B9)+([char]0x30AF)+([char]0x306E)+([char]0x30C6)+([char]0x30B9)+([char]0x30C8); Done=$false; DueDate=(Get-Date).AddDays(1).ToString("yyyy-MM-dd") }
)
}
$script:Tasks = [System.Collections.ArrayList]@(Load-Tasks)
$script:DataFilePath = $script:DataFile
# --- Shared state via hashtable (closure-safe) ---
$app = @{
Tasks = $script:Tasks
DataFile = $script:DataFilePath
}
function Save-TasksTo {
param($Tasks, $Path)
$Tasks | ConvertTo-Json -Depth 3 | Out-File $Path -Encoding UTF8 -Force
}
# --- Toast ---
function Show-ToastNotification {
param([string]$Title, [string]$Message)
try {
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom, ContentType = WindowsRuntime] | Out-Null
$template = @"
<toast duration="long">
<visual>
<binding template="ToastGeneric">
<text>$Title</text>
<text>$Message</text>
</binding>
</visual>
</toast>
"@
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
$xml.LoadXml($template)
$toast = New-Object Windows.UI.Notifications.ToastNotification $xml
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("TaskWidget").Show($toast)
} catch {
try {
$balloon = New-Object System.Windows.Forms.NotifyIcon
$balloon.Icon = [System.Drawing.SystemIcons]::Information
$balloon.BalloonTipTitle = $Title
$balloon.BalloonTipText = $Message
$balloon.Visible = $true
$balloon.ShowBalloonTip(5000)
Start-Sleep -Seconds 6
$balloon.Dispose()
} catch {}
}
}
function Check-DueReminders {
$today = (Get-Date).Date
foreach ($task in $app.Tasks) {
if (-not $task.Done -and $task.DueDate) {
try {
$due = [DateTime]::Parse($task.DueDate)
$diff = ($due.Date - $today).Days
$lToday = ([char]0x4ECA)+([char]0x65E5)+([char]0x304C)+([char]0x671F)+([char]0x9650)+([char]0xFF01)
$lTomorrow = ([char]0x660E)+([char]0x65E5)+([char]0x304C)+([char]0x671F)+([char]0x9650)
$lOverdue = ([char]0x671F)+([char]0x9650)+([char]0x8D85)+([char]0x904E)+([char]0xFF01)
$lDays = ([char]0x65E5)+([char]0x8D85)+([char]0x904E)
if ($diff -eq 0) { Show-ToastNotification $lToday $task.Text }
elseif ($diff -eq 1) { Show-ToastNotification $lTomorrow $task.Text }
elseif ($diff -lt 0) { Show-ToastNotification $lOverdue "$($task.Text) ($(([Math]::Abs($diff)))$lDays)" }
} catch {}
}
}
}
# --- XAML ---
[xml]$xaml = @"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="TaskWidget" Width="340" Height="520"
WindowStyle="None" AllowsTransparency="True" Background="Transparent"
Topmost="True" ShowInTaskbar="False" ResizeMode="NoResize">
<Border CornerRadius="12" Background="#E8141422" Margin="8">
<Border.Effect>
<DropShadowEffect BlurRadius="12" ShadowDepth="2" Opacity="0.5"/>
</Border.Effect>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Border Name="DragArea" Grid.Row="0" CornerRadius="12,12,0,0"
Background="#2D2D48" Padding="16,12">
<StackPanel IsHitTestVisible="False">
<StackPanel Orientation="Horizontal">
<TextBlock Name="ClockText" Text="00:00" FontSize="36"
FontWeight="Light" Foreground="#E6E6F0" FontFamily="Segoe UI"/>
<TextBlock Name="SecondsText" Text=":00" FontSize="18"
FontWeight="Light" Foreground="#A0A0B4"
VerticalAlignment="Bottom" Margin="3,0,0,6"/>
</StackPanel>
<TextBlock Name="DateText" Text="" FontSize="12"
Foreground="#A0A0B4" FontFamily="Segoe UI"/>
</StackPanel>
</Border>
<Border Grid.Row="1" Height="1" Background="#3C3C50" Margin="16,0"/>
<StackPanel Grid.Row="2" Margin="12,10">
<DockPanel>
<Button Name="AddButton" DockPanel.Dock="Right" Content="+"
Width="34" Height="34" Margin="6,0,0,0"
Background="#64B4FF" Foreground="White" FontSize="16" FontWeight="Bold"/>
<TextBox Name="TaskInput" Background="#2A2A3E" Foreground="#E6E6F0"
BorderBrush="#4A4A6A" BorderThickness="1" Padding="8,6"
FontSize="12" FontFamily="Segoe UI" VerticalContentAlignment="Center"/>
</DockPanel>
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<TextBlock Name="DueDateLabel" Foreground="#A0A0B4" FontSize="11"
VerticalAlignment="Center" Margin="0,0,4,0"/>
<TextBox Name="DueDateInput" Width="100" Background="#2A2A3E"
Foreground="#E6E6F0" BorderBrush="#4A4A6A" BorderThickness="1"
Padding="4,3" FontSize="11" FontFamily="Segoe UI"/>
<TextBlock Text=" (yyyy-MM-dd)" Foreground="#606078" FontSize="10"
VerticalAlignment="Center"/>
</StackPanel>
</StackPanel>
<ScrollViewer Grid.Row="3" Margin="4,0" VerticalScrollBarVisibility="Auto">
<StackPanel Name="TaskList" Margin="8,0"/>
</ScrollViewer>
<DockPanel Grid.Row="4" Margin="12,8">
<Button Name="CloseBtn" DockPanel.Dock="Right" Content="✕"
Width="28" Height="28" Background="#33FFFFFF" Foreground="#A0A0B4"
FontSize="12" BorderThickness="0"/>
<Button Name="MinimizeBtn" DockPanel.Dock="Right" Content="─"
Width="28" Height="28" Margin="0,0,4,0" Background="#33FFFFFF"
Foreground="#A0A0B4" FontSize="14" BorderThickness="0"/>
<TextBlock Name="TaskCount" Text="0" Foreground="#A0A0B4"
FontSize="11" VerticalAlignment="Center"/>
</DockPanel>
</Grid>
</Border>
</Window>
"@
$reader = New-Object System.Xml.XmlNodeReader $xaml
$window = [System.Windows.Markup.XamlReader]::Load($reader)
$clockText = $window.FindName("ClockText")
$secondsText = $window.FindName("SecondsText")
$dateText = $window.FindName("DateText")
$taskInput = $window.FindName("TaskInput")
$dueDateInput = $window.FindName("DueDateInput")
$dueDateLabel = $window.FindName("DueDateLabel")
$addButton = $window.FindName("AddButton")
$taskList = $window.FindName("TaskList")
$taskCount = $window.FindName("TaskCount")
$minimizeBtn = $window.FindName("MinimizeBtn")
$closeBtn = $window.FindName("CloseBtn")
$dragArea = $window.FindName("DragArea")
$dueDateLabel.Text = ([char]0x671F)+([char]0x9650)+":"
$dragArea.Add_MouseLeftButtonDown({ $window.DragMove() })
$closeBtn.Add_Click({ $window.Close() })
$minimizeBtn.Add_Click({ $window.WindowState = "Minimized" })
# --- Local refs for closures (NO $script: inside GetNewClosure) ---
$tasksRef = $app.Tasks
$dataFileRef = $app.DataFile
$taskListRef = $taskList
$taskCountRef = $taskCount
$taskInputRef = $taskInput
$dueDateRef = $dueDateInput
$script:RenderTaskList = {
$taskListRef.Children.Clear()
$remaining = 0
$renderRef = $script:RenderTaskList
for ($i = 0; $i -lt $tasksRef.Count; $i++) {
$task = $tasksRef[$i]
$idx = $i
$taskId = $task.Id
if (-not $task.Done) { $remaining++ }
$rowBorder = New-Object System.Windows.Controls.Border
$rowBorder.CornerRadius = [System.Windows.CornerRadius]::new(6)
$rowBorder.Padding = [System.Windows.Thickness]::new(4, 6, 4, 6)
$rowBorder.Margin = [System.Windows.Thickness]::new(0, 2, 0, 2)
if ($task.Done) {
$rowBorder.Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#1A1A2A")
} else {
$rowBorder.Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#222238")
}
$row = New-Object System.Windows.Controls.DockPanel
# --- Move up/down ---
$movePanel = New-Object System.Windows.Controls.StackPanel
$movePanel.VerticalAlignment = "Center"
$movePanel.Margin = [System.Windows.Thickness]::new(0, 0, 2, 0)
[System.Windows.Controls.DockPanel]::SetDock($movePanel, "Left")
$upBtn = New-Object System.Windows.Controls.Button
$upBtn.Content = ([char]0x25B2)
$upBtn.FontSize = 8
$upBtn.Background = [System.Windows.Media.Brushes]::Transparent
$upBtn.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#A0A0B4")
$upBtn.BorderThickness = [System.Windows.Thickness]::new(0)
$upBtn.Cursor = [System.Windows.Input.Cursors]::Hand
$upBtn.Padding = [System.Windows.Thickness]::new(3, 0, 3, 0)
$upBtn.Tag = $idx
$upBtn.Add_Click({
param($s, $e)
$ci = $s.Tag
if ($ci -gt 0) {
$item = $tasksRef[$ci]
$tasksRef.RemoveAt($ci)
$tasksRef.Insert($ci - 1, $item)
Save-TasksTo $tasksRef $dataFileRef
& $renderRef
}
}.GetNewClosure())
$downBtn = New-Object System.Windows.Controls.Button
$downBtn.Content = ([char]0x25BC)
$downBtn.FontSize = 8
$downBtn.Background = [System.Windows.Media.Brushes]::Transparent
$downBtn.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#A0A0B4")
$downBtn.BorderThickness = [System.Windows.Thickness]::new(0)
$downBtn.Cursor = [System.Windows.Input.Cursors]::Hand
$downBtn.Padding = [System.Windows.Thickness]::new(3, 0, 3, 0)
$downBtn.Tag = $idx
$downBtn.Add_Click({
param($s, $e)
$ci = $s.Tag
if ($ci -lt ($tasksRef.Count - 1)) {
$item = $tasksRef[$ci]
$tasksRef.RemoveAt($ci)
$tasksRef.Insert($ci + 1, $item)
Save-TasksTo $tasksRef $dataFileRef
& $renderRef
}
}.GetNewClosure())
$movePanel.Children.Add($upBtn) | Out-Null
$movePanel.Children.Add($downBtn) | Out-Null
# --- Delete ---
$delBtn = New-Object System.Windows.Controls.Button
$delBtn.Content = "X"
$delBtn.FontSize = 11
$delBtn.Background = [System.Windows.Media.Brushes]::Transparent
$delBtn.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#A0A0B4")
$delBtn.BorderThickness = [System.Windows.Thickness]::new(0)
$delBtn.Cursor = [System.Windows.Input.Cursors]::Hand
$delBtn.Padding = [System.Windows.Thickness]::new(2)
$delBtn.VerticalAlignment = "Center"
$delBtn.Opacity = 0.5
$delBtn.Tag = $taskId
[System.Windows.Controls.DockPanel]::SetDock($delBtn, "Right")
$delBtn.Add_Click({
param($s, $e)
$id = $s.Tag
$toRemove = $null
foreach ($t in $tasksRef) {
if ($t.Id -eq $id) { $toRemove = $t; break }
}
if ($toRemove) {
$tasksRef.Remove($toRemove) | Out-Null
Save-TasksTo $tasksRef $dataFileRef
& $renderRef
}
}.GetNewClosure())
$delBtn.Add_MouseEnter({ $this.Opacity = 1.0 })
$delBtn.Add_MouseLeave({ $this.Opacity = 0.5 })
# --- Check ---
if ($task.Done) { $mark = [char]0x2705 } else { $mark = [char]0x2B1C }
$checkBtn = New-Object System.Windows.Controls.Button
$checkBtn.Content = $mark
$checkBtn.FontSize = 14
$checkBtn.Background = [System.Windows.Media.Brushes]::Transparent
$checkBtn.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#A0A0B4")
$checkBtn.BorderThickness = [System.Windows.Thickness]::new(0)
$checkBtn.Cursor = [System.Windows.Input.Cursors]::Hand
$checkBtn.Padding = [System.Windows.Thickness]::new(2)
$checkBtn.VerticalAlignment = "Center"
$checkBtn.Width = 28
$checkBtn.Tag = $taskId
[System.Windows.Controls.DockPanel]::SetDock($checkBtn, "Left")
$checkBtn.Add_Click({
param($s, $e)
$id = $s.Tag
foreach ($t in $tasksRef) {
if ($t.Id -eq $id) { $t.Done = -not $t.Done; break }
}
Save-TasksTo $tasksRef $dataFileRef
& $renderRef
}.GetNewClosure())
# --- Text ---
$textPanel = New-Object System.Windows.Controls.StackPanel
$textPanel.VerticalAlignment = "Center"
$textPanel.Margin = [System.Windows.Thickness]::new(4, 0, 4, 0)
$textBlock = New-Object System.Windows.Controls.TextBlock
$textBlock.Text = $task.Text
$textBlock.FontSize = 12
$textBlock.FontFamily = [System.Windows.Media.FontFamily]::new("Segoe UI")
$textBlock.TextWrapping = "Wrap"
if ($task.Done) {
$textBlock.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#606078")
$textBlock.TextDecorations = [System.Windows.TextDecorations]::Strikethrough
} else {
$textBlock.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#E6E6F0")
}
$textPanel.Children.Add($textBlock) | Out-Null
if ($task.DueDate -and -not $task.Done) {
$dueBlock = New-Object System.Windows.Controls.TextBlock
$dueBlock.FontSize = 10
$dueBlock.FontFamily = [System.Windows.Media.FontFamily]::new("Segoe UI")
try {
$due = [DateTime]::Parse($task.DueDate)
$diff = ($due.Date - (Get-Date).Date).Days
$sOv = ([char]0x671F)+([char]0x9650)+([char]0x8D85)+([char]0x904E)+": "
$sDs = ([char]0x65E5)+([char]0x8D85)+([char]0x904E)
$sTd = ([char]0x4ECA)+([char]0x65E5)+([char]0x307E)+([char]0x3067)
$sTm = ([char]0x660E)+([char]0x65E5)+([char]0x307E)+([char]0x3067)
$sDl = ([char]0x65E5)+([char]0x5F8C)
if ($diff -lt 0) {
$dueBlock.Text = "$sOv$(([Math]::Abs($diff)))$sDs"
$dueBlock.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#FF6464")
} elseif ($diff -eq 0) {
$dueBlock.Text = $sTd
$dueBlock.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#FFB040")
} elseif ($diff -eq 1) {
$dueBlock.Text = $sTm
$dueBlock.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#FFB040")
} else {
$dueBlock.Text = "${diff}$sDl ($($due.ToString('M/d')))"
$dueBlock.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#A0A0B4")
}
} catch {
$kl = ([char]0x671F)+([char]0x9650)+": "
$dueBlock.Text = "$kl$($task.DueDate)"
$dueBlock.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#A0A0B4")
}
$textPanel.Children.Add($dueBlock) | Out-Null
}
$row.Children.Add($movePanel) | Out-Null
$row.Children.Add($delBtn) | Out-Null
$row.Children.Add($checkBtn) | Out-Null
$row.Children.Add($textPanel) | Out-Null
$rowBorder.Child = $row
$taskListRef.Children.Add($rowBorder) | Out-Null
}
$lbl = ([char]0x306E)+([char]0x672A)+([char]0x5B8C)+([char]0x4E86)+([char]0x30BF)+([char]0x30B9)+([char]0x30AF)
$taskCountRef.Text = "$remaining $lbl"
}
# --- Add task (also uses local refs only) ---
$addButton.Add_Click({
$text = $taskInputRef.Text.Trim()
if ($text -eq "") { return }
$newTask = [PSCustomObject]@{
Id = [guid]::NewGuid().ToString()
Text = $text
Done = $false
DueDate = $dueDateRef.Text.Trim()
}
$tasksRef.Add($newTask) | Out-Null
Save-TasksTo $tasksRef $dataFileRef
$taskInputRef.Text = ""
$dueDateRef.Text = ""
& $script:RenderTaskList
})
$taskInput.Add_KeyDown({
param($s, $e)
if ($e.Key -eq "Return") {
$text = $taskInputRef.Text.Trim()
if ($text -eq "") { return }
$newTask = [PSCustomObject]@{
Id = [guid]::NewGuid().ToString()
Text = $text
Done = $false
DueDate = $dueDateRef.Text.Trim()
}
$tasksRef.Add($newTask) | Out-Null
Save-TasksTo $tasksRef $dataFileRef
$taskInputRef.Text = ""
$dueDateRef.Text = ""
& $script:RenderTaskList
}
})
# --- Clock ---
$timer = New-Object System.Windows.Threading.DispatcherTimer
$timer.Interval = [TimeSpan]::FromSeconds(1)
$timer.Add_Tick({
$now = Get-Date
$clockText.Text = $now.ToString("HH:mm")
$secondsText.Text = ":" + $now.ToString("ss")
$y = ([char]0x5E74); $mo = ([char]0x6708); $dy = ([char]0x65E5)
$dow = switch ($now.DayOfWeek) {
"Sunday" { ([char]0x65E5) }
"Monday" { ([char]0x6708) }
"Tuesday" { ([char]0x706B) }
"Wednesday" { ([char]0x6C34) }
"Thursday" { ([char]0x6728) }
"Friday" { ([char]0x91D1) }
"Saturday" { ([char]0x571F) }
}
$dateText.Text = "$($now.Year)$y$($now.Month)$mo$($now.Day)$dy ($dow)"
})
$timer.Start()
# --- Init ---
$window.Add_Loaded({
$screen = [System.Windows.SystemParameters]::WorkArea
$window.Left = $screen.Right - $window.Width - 16
$window.Top = $screen.Bottom - $window.Height - 16
& $script:RenderTaskList
Check-DueReminders
})
$window.ShowDialog() | Out-Null
Launch-TaskWidget.bat
@echo off
powershell -ExecutionPolicy Bypass -WindowStyle Hidden -File "%~dp0TaskWidget.ps1"