2 Commits

Author SHA1 Message Date
justin 2aa14642a0 GUI: hide-to-tray on X button; tray persists until explicit Exit
The minimize-to-tray behavior already worked, but clicking the X button
killed the GUI process and took the tray with it. That made "tray when
the GUI window is closed" a UX dead end - the only way to get the tray
was to leave the window minimized.

Now:
  - X button / Alt+F4 -> hide window, tray stays alive
  - Tray double-click -> reopens window
  - File -> Exit (or tray's Exit menu) -> truly quits the process

Wired by adding a RealExitRequested event on MainViewModel that the
window subscribes to (so File -> Exit sets the ExitForReal flag before
calling Shutdown), and a parallel onExit callback on TrayIcon for the
tray menu's Exit item. The Closing handler checks ExitForReal: if
false (X / Alt+F4) it cancels the close and hides; if true, it disposes
the tray and lets the close proceed.

Auto-start at login is still TBD - if you want the tray to be there
without manually launching the GUI after a reboot, that's a separate
Task Scheduler entry. Skipping for now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 11:26:49 -04:00
justin 9fcff2694a Wiki sync: stop treating git's stderr warnings as fatal (#5)
PowerShell with ErrorActionPreference=Stop escalates ANY native-command
stderr output to a script-terminating error. git writes plenty of
informational lines to stderr (CRLF nags, "remote: Processed N
references", "Switched to branch X"), which made the sync script
abort partway through every run when actually nothing was wrong.

Three fixes:

1. Switch to ErrorActionPreference=Continue and check $LASTEXITCODE
   manually after each git call.
2. Drain stderr on each git invocation with `2>&1 | Out-Null`.
3. Disable core.autocrlf and core.safecrlf in the throwaway wiki
   clone so git stops complaining about line endings.

Verified end-to-end against Gitea: 12 pages + sidebar pushed cleanly.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 11:19:56 -04:00
4 changed files with 61 additions and 13 deletions
+20 -8
View File
@@ -44,7 +44,12 @@ param(
[string]$AuthorEmail = 'noreply@jpaul.me' [string]$AuthorEmail = 'noreply@jpaul.me'
) )
$ErrorActionPreference = 'Stop' # Continue (not Stop) because git writes informational messages to stderr
# (CRLF warnings, "remote: Processed N references" etc.) which PowerShell 5.1
# escalates to a script-fatal error under Stop. We check $LASTEXITCODE
# manually after each git call instead.
$ErrorActionPreference = 'Continue'
$repoRoot = Split-Path -Parent $PSScriptRoot $repoRoot = Split-Path -Parent $PSScriptRoot
$docsDir = Join-Path $repoRoot 'docs' $docsDir = Join-Path $repoRoot 'docs'
$workDir = Join-Path $env:TEMP ("webhook-wiki-{0}" -f ([guid]::NewGuid().ToString('N').Substring(0, 8))) $workDir = Join-Path $env:TEMP ("webhook-wiki-{0}" -f ([guid]::NewGuid().ToString('N').Substring(0, 8)))
@@ -98,10 +103,14 @@ function New-Sidebar() {
# 1. Clone the wiki. # 1. Clone the wiki.
Write-Host "Cloning wiki to $workDir..." Write-Host "Cloning wiki to $workDir..."
git clone --quiet $WikiUrl $workDir & git clone --quiet $WikiUrl $workDir 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
throw "git clone failed. Has the wiki been initialized? Visit the repo's Wiki tab and create the first page via the UI before running this script." throw "git clone failed. Has the wiki been initialized? Visit the repo's Wiki tab and create the first page via the UI before running this script."
} }
# Suppress git's CRLF nags for this throwaway clone so they don't become
# "errors" via PowerShell's native-command stderr handling.
& git -C $workDir config core.autocrlf false 2>&1 | Out-Null
& git -C $workDir config core.safecrlf false 2>&1 | Out-Null
try { try {
Push-Location $workDir Push-Location $workDir
@@ -128,16 +137,19 @@ try {
# 4. Sidebar # 4. Sidebar
Set-Content -LiteralPath (Join-Path $workDir '_Sidebar.md') -Value (New-Sidebar) -Encoding utf8 -NoNewline Set-Content -LiteralPath (Join-Path $workDir '_Sidebar.md') -Value (New-Sidebar) -Encoding utf8 -NoNewline
# 5. Commit + push if anything actually changed. # 5. Commit + push if anything actually changed. Drain stderr from each
git add -A # git invocation so PowerShell doesn't treat warnings as errors.
$changes = git status --porcelain & git add -A 2>&1 | Out-Null
$changes = & git status --porcelain 2>&1
if (-not $changes) { if (-not $changes) {
Write-Host "Wiki already up to date." Write-Host "Wiki already up to date."
return return
} }
$sha = git -C $repoRoot rev-parse --short HEAD $sha = & git -C $repoRoot rev-parse --short HEAD 2>&1
git -c "user.name=$AuthorName" -c "user.email=$AuthorEmail" commit -q -m "Sync from docs/ at $sha" & git -c "user.name=$AuthorName" -c "user.email=$AuthorEmail" commit -q -m "Sync from docs/ at $sha" 2>&1 | Out-Null
git push --quiet if ($LASTEXITCODE -ne 0) { throw "git commit failed (exit $LASTEXITCODE)" }
& git push --quiet 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) { throw "git push failed (exit $LASTEXITCODE)" }
Write-Host "Pushed updated wiki." Write-Host "Pushed updated wiki."
} }
finally { Pop-Location } finally { Pop-Location }
+32 -2
View File
@@ -1,3 +1,4 @@
using System.ComponentModel;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
@@ -11,19 +12,48 @@ public partial class MainWindow : Window
private readonly TrayIcon _tray; private readonly TrayIcon _tray;
private readonly MainViewModel _vm; private readonly MainViewModel _vm;
/// <summary>
/// Set to true when the user has explicitly asked to quit (File -> Exit or
/// Tray -> Exit). The OnClosing handler reads this to decide whether to
/// actually let the window close or hide it to the tray.
/// </summary>
public bool ExitForReal { get; set; }
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
_vm = new MainViewModel(new AdminPipeClient()); _vm = new MainViewModel(new AdminPipeClient());
DataContext = _vm; DataContext = _vm;
_vm.RealExitRequested += OnRealExitRequested;
_tray = new TrayIcon( _tray = new TrayIcon(
resolveMainWindow: () => Application.Current.MainWindow, resolveMainWindow: () => Application.Current.MainWindow,
restartServiceAsync: async () => await new AdminPipeClient().RestartListenerAsync()); restartServiceAsync: async () => await new AdminPipeClient().RestartListenerAsync(),
onExit: OnRealExitRequested);
Loaded += async (_, _) => await _vm.RefreshCommand.ExecuteAsync(null); Loaded += async (_, _) => await _vm.RefreshCommand.ExecuteAsync(null);
StateChanged += OnStateChanged; StateChanged += OnStateChanged;
Closed += (_, _) => _tray.Dispose(); Closing += OnClosing;
}
private void OnClosing(object? sender, CancelEventArgs e)
{
if (ExitForReal)
{
_tray.Dispose();
return;
}
// Treat the X button / Alt+F4 like a minimize: hide to tray, keep the
// process alive so the tray icon persists.
e.Cancel = true;
Hide();
ShowInTaskbar = false;
}
private void OnRealExitRequested()
{
ExitForReal = true;
Application.Current.Shutdown();
} }
private void OnStateChanged(object? sender, EventArgs e) private void OnStateChanged(object? sender, EventArgs e)
+4 -2
View File
@@ -16,11 +16,13 @@ public sealed class TrayIcon : IDisposable
private readonly NotifyIcon _icon; private readonly NotifyIcon _icon;
private readonly Func<Window?> _resolveMainWindow; private readonly Func<Window?> _resolveMainWindow;
private readonly Func<Task> _restartServiceAsync; private readonly Func<Task> _restartServiceAsync;
private readonly Action _onExit;
public TrayIcon(Func<Window?> resolveMainWindow, Func<Task> restartServiceAsync) public TrayIcon(Func<Window?> resolveMainWindow, Func<Task> restartServiceAsync, Action onExit)
{ {
_resolveMainWindow = resolveMainWindow; _resolveMainWindow = resolveMainWindow;
_restartServiceAsync = restartServiceAsync; _restartServiceAsync = restartServiceAsync;
_onExit = onExit;
_icon = new NotifyIcon _icon = new NotifyIcon
{ {
@@ -39,7 +41,7 @@ public sealed class TrayIcon : IDisposable
menu.Items.Add(new ToolStripSeparator()); menu.Items.Add(new ToolStripSeparator());
menu.Items.Add("&Restart service", null, async (_, _) => await _restartServiceAsync().ConfigureAwait(false)); menu.Items.Add("&Restart service", null, async (_, _) => await _restartServiceAsync().ConfigureAwait(false));
menu.Items.Add(new ToolStripSeparator()); menu.Items.Add(new ToolStripSeparator());
menu.Items.Add("E&xit", null, (_, _) => Application.Current.Shutdown()); menu.Items.Add("E&xit", null, (_, _) => _onExit());
return menu; return menu;
} }
@@ -286,10 +286,14 @@ public sealed partial class MainViewModel : ObservableObject
} }
} }
/// <summary>Raised when the user picks File -> Exit. MainWindow flips its
/// ExitForReal flag and shuts down, bypassing the X-hides-to-tray logic.</summary>
public event Action? RealExitRequested;
[RelayCommand] [RelayCommand]
private void Exit() private void Exit()
{ {
Application.Current.Shutdown(); RealExitRequested?.Invoke();
} }
[RelayCommand] [RelayCommand]