A lightweight Windows system tray application that hosts a WebView2 browser window. Designed for web apps that you want quick access to without cluttering your taskbar.
- System Tray Integration - Runs in the system tray; double-click to open and optionally return to the configured URL, close to hide, and optionally minimize to the taskbar
- WebView2 Browser - Uses Microsoft Edge WebView2 for modern web compatibility
- Configurable - Set custom URL, window title, and JavaScript hooks via GUI
- JavaScript Hooks - Execute custom JavaScript when the window is shown or hidden (useful for pausing/resuming web app activity)
- External Link Handling - Optionally open new windows/tabs (
target="_blank",window.open) in the system default browser instead of a WebView2 popup - Static Host Mappings - Optionally resolve listed hostnames to configured IP addresses inside the embedded browser without changing system DNS
- Optional Mixed Content - Explicitly allow an HTTPS page to load HTTP content from configured legacy origins
- Self Update - Compare embedded local and repository versions in the configuration dialog, install newer builds, or force a reinstall of the same version
- Lockdown Header - Optionally stamp every request with a rolling, hour-keyed
X-Lockdowntoken a gateway can require as an extra access layer - Preloaded on Startup - The page is loaded into the WebView at launch so it is ready the moment you open the window
- Hide Grace Period - Re-opening within 60 seconds of hiding returns exactly where you left off; after that the page resets to the configured URL in the background, so the next open starts fresh with no visible reload
- Self-Healing Container - Every time the window opens, the app verifies the embedded browser is actually rendering (frame heartbeat, plus a screen check after resume from sleep/hibernate) and automatically rebuilds it if it is not — no more permanently blank windows after hibernation
- Optional CPU Saving - Opt-in "sleep when inactive" suspends the web container while hidden to save CPU on laptops, and pre-emptively wakes it when you hover the tray icon
- Registry Storage - Settings persist in Windows Registry (
HKCU\SOFTWARE\JPIT\SystrayLauncher) - Single Instance - Only one instance can run at a time
- First-Launch Setup - Configuration dialog appears automatically on first run
Right-click the tray icon to access:
- Refresh - Reloads the page and brings window to foreground
- Refresh + Clear Cache - Clears browser cache and reloads
- Open - Shows the main window
- Configure - Opens the settings dialog
- Exit - Closes the application
Settings available in the Configure dialog:
| Setting | Description |
|---|---|
| Window Title | Base title shown with the configured URL hostname when available, plus a loading indicator during navigation |
| URL | The web page to load |
| Open main window maximized | Fills the available desktop whenever the main window opens. Disabled by default; when disabled, the window opens centered at 90% of the work area. |
| Show main window in the taskbar | Adds a taskbar button while the main window is open and minimizes the window there normally. Disabled by default; when disabled, minimizing hides the window to the tray. Changing this setting restarts the launcher. |
| Return to configured URL on tray double-click | When enabled, double-clicking the tray icon returns the main window to the configured URL even if the window is already open. Disable it to open or focus the window without changing the current page. Enabled by default. |
| JavaScript on Hide | JS executed when window is fully covered or hidden |
| JavaScript on Show | JS executed when window becomes visible |
| Resolve listed hostnames to static IP addresses | Bypasses normal DNS for explicitly listed hostnames inside the main web container. Enter one hostname:IP mapping per line (for example, device.local:192.168.1.20). The option is disabled by default and restarts the launcher when changed. |
| Allow listed HTTP origins on HTTPS pages | Lets an HTTPS page embed content from explicitly listed HTTP origins. Enter one origin per line (for example, http://device.local:8080). The option is disabled by default and restarts the launcher when changed. |
| Send X-Lockdown header | Adds an X-Lockdown header to every request the embedded browser makes: the request's own User-Agent encrypted with a key derived from the current UTC hour and an optional shared secret (see Lockdown Header). Toggling applies immediately. Disabled by default. |
| Open new windows in the default browser | When enabled, links that would open a new window or tab launch in the system default browser instead of a WebView2 popup. Only http(s) links are handed to the browser. Popups that must script back to the opening page (some login flows) may not work while enabled. Disabled by default. |
| Sleep web container when inactive | When enabled, suspends the WebView to save CPU while the window is hidden, and pre-emptively wakes it on tray-icon hover. The page is always preloaded at startup regardless of this setting. Disabled by default. |
| Automatically check for updates | Checks at startup, whenever Configure opens, and every 60 minutes. A newer build opens Configure and its update prompt. Enabled by default. |
| Enable debug logging | Appends timestamped diagnostic events (recovery attempts, web view rebuilds, power transitions) to %LOCALAPPDATA%\SystrayLauncher\debug.log (rotated at ~1 MB). Useful when reporting issues. Disabled by default. |
The footer displays the application and WebView2 runtime versions together as
v<application version> / <WebView2 version>.
When Automatically check for updates is enabled, SystrayLauncher checks at
startup, whenever the Configure dialog opens, and once every 60 minutes using a
single low-frequency Windows timer. A newer build opens Configure and its update
prompt; matching or older builds and failed automatic checks are silently
discarded. An automatically opened prompt offers Ignore this version, which
suppresses that version during later automatic checks, including after restart.
The manual Update button still displays every result and can install an
ignored version. Checks use the repository's
release/SystrayLauncher.exe.
The update check downloads the executable to the user's temporary directory and compares its embedded Windows file version with the running executable's version. While downloading, the button displays the current transfer speed and can be clicked again to stop the check and remove the partial download. The result dialog displays both version numbers. A newer build can be installed normally, while a matching build offers a Force update action to reinstall it; an older repository build is never installed. Installation requests standard Windows UAC approval, safely replaces the current executable, and restarts the launcher. After a successful update, the restarted launcher opens an HTML confirmation with the newly installed version. Dismissing that confirmation leaves the configuration dialog open. Cancelling the download, result dialog, or UAC prompt leaves the current version running. File size is used only to validate the download and enforce its safety limit.
When Resolve listed hostnames to static IP addresses is enabled, the main
WebView2 environment starts with Chromium --host-resolver-rules generated
from the exact mappings entered in the dialog. For example,
device.local:192.168.1.20 makes requests for device.local connect to
192.168.1.20 without modifying the Windows hosts file or affecting Edge and
other applications. The configuration dialog uses a separate WebView2
environment and is not affected.
The URL and web origin remain the configured hostname. HTTPS therefore still
requires the destination server to present a certificate valid for that
hostname, and virtual hosting continues to receive the hostname. Each
subdomain must be listed separately. IPv6 addresses are supported when wrapped
in brackets, such as device.local:[2001:db8::20]. A configured HTTP proxy can
resolve destination hostnames itself, so these local mappings are intended for
direct connections.
When the configured URL's hostname is one of the mapped entries, the main
window title shows the address currently used for it after the hostname, for
example device.local (192.168.1.20). With the DNS fallback option below this
switches live between the mapped address and the DNS-resolved address as the
route changes.
This feature uses a Chromium browser switch rather than a stable WebView2 DNS API. Microsoft documents browser flags as development-oriented and not guaranteed long-term, so the behavior should be tested when deploying a new WebView2 Runtime version.
When Fall back to standard DNS when a mapped address is unreachable is
also enabled, the launcher enforces the mappings itself instead of using
resolver rules: it runs a small forward proxy on 127.0.0.1 (random port,
loopback only) and starts the browser with --proxy-pac-url pointing at a
generated PAC script that routes only the listed hostnames through the proxy
— all other traffic stays direct, and the PAC's DIRECT fallback keeps pages
loading even if the proxy itself ever stops answering. For each hostname the
proxy first attempts a TCP connection to the mapped address with a short
timeout; if that fails, the same request is completed through normal DNS
resolution, and the mapped address is then re-tried at most once per minute
(triggered by traffic, and immediately after the machine resumes from sleep).
While the mapped address answers, all connections use it until one fails,
which switches the hostname back to DNS resolution in the same request. This
suits mappings that are only reachable from certain networks. Requests for
hostnames that are not listed are refused by the proxy, HTTPS certificates
are still validated against the hostname exactly as above, and if the proxy
cannot start the launcher falls back to the strict resolver-rules behavior.
The mixed-content option starts WebView2 with the documented
--unsafely-treat-insecure-origin-as-secure
switch for the exact origins entered in the dialog. Paste a full HTTP URL if
convenient; the dialog stores only its scheme, host, and port. Every HTTP origin
reached through an iframe redirect or used for its HTTP subresources must be
listed separately.
Treating an HTTP origin as trustworthy weakens transport security and may also
enable browser APIs that normally require a secure context. It does not bypass
Content Security Policy, X-Frame-Options/frame-ancestors, certificate
errors, or cross-origin access controls. WebView2 documents browser flags for
development use rather than as a stable production API, so this legacy
compatibility option may change with future Edge runtime versions.
When Send X-Lockdown header is enabled, every HTTP(S) request the embedded browser issues (pages, subresources, XHR/fetch, iframes, service workers on current runtimes) carries an additional header:
X-Lockdown: base64( IV[16 bytes] + AES-256-CBC( User-Agent ) )
- The plaintext is the exact
User-Agentvalue sent with that same request. - The AES key is
SHA-256( secret + "|" + "YYYY-MM-DD HH:00:00" ), where the timestamp is the current UTC hour (minutes and seconds zeroed) andsecretis the shared secret from the dialog (empty string if none is set). - The IV is random and prepended; padding is PKCS#7.
A gateway verifies by trying the keys for the previous, current, and next hour
and comparing the decryption against the request's own User-Agent header.
The rolling window means client and server clocks only have to agree to
roughly an hour. In PHP:
function lockdown_pass(string $secret): bool {
$raw = base64_decode($_SERVER['HTTP_X_LOCKDOWN'] ?? '', true);
if ($raw === false || strlen($raw) < 32) return false;
$iv = substr($raw, 0, 16);
$ciphertext = substr($raw, 16);
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
foreach ([0, -3600, 3600] as $offset) {
$key = hash('sha256', $secret . '|' . gmdate('Y-m-d H:00:00', time() + $offset), true);
$plain = openssl_decrypt($ciphertext, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
if ($plain !== false && hash_equals($plain, $ua)) return true;
}
return false;
}Notes:
- Set a shared secret. This application is open source, so without a secret the scheme is only obscurity — anyone can derive the hourly key and mint a valid header. With a secret, forging requires knowing it. The secret is compared byte-for-byte (UTF-8, whitespace-trimmed by the dialog), so use the identical string on both sides.
- The token is replayable within its roughly two-hour validity window by anyone who observes it (including third-party hosts the page embeds), and it authenticates only the launcher, not a user. Treat it as one extra gate in front of your normal authentication, not a replacement for it.
- WebSocket connections are not intercepted by WebView2 and never carry the header, so enforce it on ordinary HTTP(S) endpoints only.
The application uses a single icon file (icon.ico) that appears in multiple locations:
| Location | Description |
|---|---|
| System Tray | Small icon in the notification area (16x16 or 32x32 depending on DPI) |
| Window Title Bar | Icon shown in the top-left corner of the main window |
| Taskbar | Optional icon displayed while the main window is visible when Show main window in the taskbar is enabled |
| Alt-Tab Switcher | Icon shown when cycling through windows |
- Replace
icon.svgwith your own SVG file - Run
make iconto generateicon.ico(requires ImageMagick) - Rebuild with
make
This generates a multi-resolution .ico containing 16x16, 24x24, 32x32, 48x48, and 256x256 sizes, covering all DPI scaling levels. The icon is embedded into the executable at compile time via resource.rc.
- Windows 10/11
- WebView2 Runtime (usually pre-installed on Windows 10/11)
Requires MinGW-w64 cross-compiler and the WebView2 SDK.
# Download WebView2 SDK (one-time)
make deps
# Build
make
# Output: SystrayLauncher.exe + WebView2Loader.dll in release/