- C# 97.1%
- PowerShell 2.1%
- Shell 0.8%
| .claude/skills/verify | ||
| .forgejo/workflows | ||
| .idea/.idea.WayfarerConsole/.idea | ||
| docs | ||
| packaging | ||
| WayfarerConsole | ||
| .gitattributes | ||
| .gitignore | ||
| README.md | ||
| WayfarerConsole.sln | ||
Wayfarer Console
Quick Start
Wayfarer Console is a standalone, retro-future desktop terminal built with C#, .NET, and Avalonia. It provides a highly themed, cinematic shell experience for Windows and Linux that runs before your actual shell process even starts.
Key Features
- Cinematic Boot Experience: Customizable animated ASCII/text splash screen.
- Persona-Driven Theming: Switch between complete visual identities (e.g., PipBoy, Starware).
- Advanced Visual Effects: Toggleable CRT scanlines, phosphor glow, and orbital backgrounds.
- True Shell Integration: High-performance shell spawning via
Porta.Pty. - Cross-Platform Notifications: Themed popups on Windows; D-Bus notifications on Linux.
Installation
Windows
- Download the latest release
.zip. - Run
Install.ps1in a PowerShell terminal:powershell -ExecutionPolicy Bypass -File .\Install.ps1
Linux
- Download the latest release
.tar.gz. - Run the installation script:
tar -xzf Wayfarer-*-linux-x64.tar.gz && cd wayfarer-console && ./install.sh
Building from Source
Requires the .NET SDK.
dotnet build WayfarerConsole/WayfarerConsole.csproj
that hosts a real shell inside a themed retro-future "skin" — pwsh on
Windows, your own native $SHELL (bash/zsh) on Linux/macOS — complete with
a custom pre-shell boot animation, a 16-color palette per persona, a Nerd
Font, and toggleable CRT/phosphor visual effects. Built as the piece a
regular Windows Terminal profile can't do: something that runs before any
shell process exists.
Two personas ship today: PipBoy (Fallout-style green monochrome) and Starware (a cooler sci-fi blue/gold palette).
Features
- Your real shell underneath. Spawns pwsh on Windows, or your own native
$SHELLon Linux/macOS, via a pty (Porta.Pty); everything you'd normally do in a terminal — history/completion, colors, scrollback — works as-is. The terminal screen buffer is parsed with VtNetCore and painted with a custom cell-gridDrawingContextrenderer (not aTextBox), so full per-cell color/attribute control comes for free. - Animated boot splash, tied to real startup work. Before/while the
shell spawns, an in-window sequence (ASCII art, "INITIALIZING..." lines,
progress bars, a random flavor quote) plays out with typewriter timing.
The pty actually starts concurrently with the splash, not after it — a
progress-bar check can optionally name a real
step, in which case its fill is tied to a matching marker your shell integration emits (see Boot-step markers below) instead of faking a duration; checks with nostepstay purely cosmetic. A final safety-net wait ("ready") always gates the handoff to the terminal, so the splash never drops you into a shell that's still mid-setup. Each persona also picks its own presentation viabootStylein its manifest — a static/noise burst before the art types in, a boot sound, and how it hands off to the terminal (fade,static-flicker, or a directionalwipe). No shell process exists yet when this starts — this is the part a Windows Terminal profile fundamentally cannot do, since Windows Terminal always launches straight into a shell. - Manifest-based theming. Each persona is a single self-contained JSON
file under
Theme/Manifests/bundling palette, font, header/subtitle text, boot sequence content and timing, and default visual effects — nothing to wire up by hand across separate theme/boot-sequence files. Pick the active one viaactiveManifestin your per-userapp.config.json(see Configuration below for its actual path). - Personalized cursor and background, set via first-run quick-pick steps
(
Boot/ProfileRecoveryRunner.cs) and persisted per-profile independent of the active theme/preset: cursor shape/color/blink (cursor.*), and an optional solid-color or image background layer with acrylic tint and an unfocused-window dim (background.*) — see Configuration below for the full schema. - Toggleable visual effects. An extensible, per-manifest effects
registry (
Effects/) that applies to the whole window for the whole session:crt— scanline overlay with a subtle looping flicker, plus a vignette.glow— soft phosphor bloom on the terminal text, colored from the active theme. Each manifest opts into its own defaults (PipBoy:crt+glow; Starware:glowonly). Toggle any effect at runtime by typing:crt,:glow, or:effects(lists all effects and their on/off state) and pressing Enter — these are intercepted client-side before ever reaching the shell, so they never show up in your shell history or run as a real command. Feedback appears in a small reserved strip between the header and the terminal, not overlaid on your session's output. Type:helpfor the full command list, including:orbit/:orbitspeedbelow.
- Desktop notifications. On Windows, a themed, borderless popup (not a
native toast, so it can be fully restyled) fires when the window isn't
focused. On Linux, this instead fires a real desktop notification via
org.freedesktop.Notificationsover D-Bus, so it shows up through whatever notification system the desktop already uses:- Command finished — after a command that ran longer than a configurable threshold, with duration/exit code/command text.
- Needs input — when a wrapped interactive prompt (e.g.
Read-Host, or a shadowedreadon bash/zsh) is waiting on you; persists until dismissed or handled. On Windows, notifications stack (newest prominent, older ones offset behind like a card deck) and animate out with a "collapsing into a black hole" scale/rotate/fade; clicking one both dismisses it and focuses the main window. On Linux, clicking the native notification focuses the main window; dismissal/stacking is owned by the desktop's own notification daemon. The taskbar/title-bar icon also swaps to an alert variant on any terminal bell while unfocused, resetting once the window is activated. All of this is driven by a private marker protocol emitted from Wayfarer's own independent shell integration (Send-WayfarerMarkeron prompt in pwsh's case, the equivalent__wayfarer_markerin bash/zsh's case; aRead-Host/readproxy for needs-input — see Boot-step markers below for why this is a separate profile/rc file, not your real one) and stripped out of the raw output stream before VtNetCore ever parses it — nothing shows up as literal terminal text.
- Boot sound. Each persona can play a short sound once at boot start —
PipBoy gets a CRT static burst, Starware a clean retro-terminal beep
sequence — muteable globally via
bootSoundEnabledregardless of what the active manifest opts into. See Sound assets below. - Mouse-wheel scrollback, using VtNetCore's screen-buffer viewport
directly (no
ScrollViewerinvolved). - Mouse selection and clipboard, conhost/PowerShell-console style:
click-drag selects a range (double-click a word, triple-click a line),
extracted straight from VtNetCore's buffer rather than any hidden text
control.
Ctrl+Shift+C/Ctrl+Shift+Vcopy/paste (plainCtrl+C/Ctrl+Vstay untouched, since the shell already owns those as SIGINT/paste-char). Right-click copies the active selection (and clears it), or pastes if nothing's selected. Pasted text is normalized to bare CR line endings, matching what PSReadLine/readline expect from Enter. - Decorative orbit background. A starfield plus a solar-system diagram
rendered behind the boot splash and terminal (
Controls/OrbitBackground.cs) — ambient mode (simple constant-speed circles) by default, or toggle to "real" mode for actual J2000 Keplerian orbital mechanics (real eccentric orbits, to-scale relative body sizes) via the:orbitcommand;:orbitspeedswaps real mode's clock between live wall-clock ("current sky") and a compressed simulated one. Purely decorative — themed from the active palette, re-resolved live if the theme changes. - CI-built releases. A Forgejo Actions pipeline publishes self-contained
win-x64andlinux-x64single-file builds and attaches both to a GitHub Release-style tag push — no manual publish step.
Configuration
Your own settings/profile (app.config.json — auto-created on first run,
never hand-created) live in the same per-user, per-OS location as the shell
integration files above, not next to the exe: %LocalAppData%\WayfarerConsole\app.config.json
on Windows, ~/.config/wayfarer-console/app.config.json (or
$XDG_CONFIG_HOME if set) on Linux/macOS. See Configuration/AppDirectories.cs
/ Configuration/ConfigLoader.cs::AppConfigPath. The bundled Theme/Manifests/
and Theme/Presets/ (palettes, boot personas) stay next to the exe — those
are app resources shipped with the install, not your data.
A brand-new profile is seeded from Theme/DefaultProfile.json — a bundled
template (the Starware-based config that used to live at the real runtime
app.config.json path before that moved to a per-user location above;
anything it doesn't set falls back to AppConfig's own field defaults)
that ConfigLoader.LoadDefaultProfileTemplate() loads instead of a bare
default whenever no profile exists yet or the real one fails to parse.
This is what the first-run/recovery flow's "profile template" scaffold
stage writes, and what App.axaml.cs itself falls back to picking a persona
from — so a fresh install's actual default persona/settings are just this
one JSON file, not a hardcoded C# value.
// app.config.json
{
"activeManifest": "starware",
"activePreset": null,
"skipBootAnimation": false,
"bootSoundEnabled": true,
"operatorName": "Korbinian",
"operatorId": "a1b2c3d4",
"notifications": {
"enabled": true,
"notifyOnCommandFinished": true,
"notifyOnNeedsInput": true,
"minCommandDurationMs": 3000,
"finishedDisplayMs": 6000,
"corner": "BottomRight",
"offsetX": 16,
"offsetY": 16,
"showCommandText": true,
"showExitCode": true
},
"cursor": {
"shape": "Block",
"colorOverride": null,
"heightPercent": 0.25,
"blinkRateMs": null
},
"background": {
"solidColorOverride": null,
"imagePath": null,
"imageStretch": "UniformToFill",
"imageAlignment": "Center,Center",
"imageOpacity": 1.0,
"acrylicEnabled": false,
"acrylicTintOpacity": 0.6,
"unfocusedDimOpacity": null
}
}
activeManifest— which file underTheme/Manifests/to load (currentlypipboyorstarware).activePreset— id of aTheme/Presets/{id}.jsonwhose palette/font/cursor replaces the active manifest's own wholesale, ornullto use the manifest's palette. (Persona identity — art, boot checks, quotes, effects — always comes from the manifest either way.)skipBootAnimation— skip straight to the terminal (also skips the boot sound).bootSoundEnabled— master mute for boot sound, independent of what the active manifest'sbootStyle.soundopts into.operatorName/operatorId— display name and a stable per-profile id, both set once during first-run and never regenerated afterward.notifications.*— enable/disable per trigger type, thresholds, popup corner/offset, and how much detail to show in the body.cursor.*— shape (Block/Underline/Bar, plusVintage/EmptyBox/DoubleUnderscorewith no native VT equivalent), an optional color override, and blink rate; independent of the active theme/preset.background.*— an optional solid-color or image layer behind the terminal (stretch/alignment/opacity), an optional acrylic tint, and an optional dim multiplier while the window is unfocused.
activePreset, operatorName/operatorId, cursor, and background are
all set via first-run quick-pick steps (see Boot/ProfileRecoveryRunner.cs)
rather than being hand-edited normally — shown here so the full schema is in
one place, not because you're expected to write this file directly.
Adding a persona
Drop a new Theme/Manifests/{name}.json (palette, font, boot sequence
text/timing, defaultEffects) — see pipboy.json/starware.json for the
full schema — and set activeManifest to its filename (without .json).
Each check in checks is now an object, not a bare string:
"checks": [
{ "label": "CHECKING RADIATION LEVELS" },
{ "label": "SYNCING HOMELAB LXC CONTAINERS", "step": "lxc-sync" }
]
label is always shown; step, if present, ties that bar's fill to a real
bootstep marker instead of faking a duration — see Boot-step markers
below. Leave it out for purely cosmetic checks (most of them).
bootStyle controls presentation, separate from content:
"bootStyle": {
"staticBurst": true,
"sound": "pipboy-static.wav",
"transition": "static"
}
staticBurst— brief noise-glyph flicker before the ASCII art types in.sound— filename underAssets/Sounds/, or omit/nullfor silence.transition—"fade"(plain opacity cross-fade),"static"(reuses the noise-glyph burst as a "channel change" flicker), or"wipe"(a directional left-to-right reveal).
Boot-step markers (shell integration wiring)
Provisioned automatically on every launch, into Wayfarer's own
independent profile/rc file — never your real one. Earlier versions of this
wired straight into your actual $PROFILE/.bashrc/.zshrc, but that
meant your own prompt/color/alias/module customizations visibly took over
once your real profile finished loading (a themed terminal skin fighting
your own theme, mid-session), and however long your profile takes to load
became this app's startup lag too. Now PtySession points the shell at a
Wayfarer-owned file directly and exclusively, so your real profile is never
read at all:
- Windows (pwsh): generates
%LocalAppData%\WayfarerConsole\Wayfarer.Integration.ps1(implementing every marker below:Send-WayfarerMarker,Send-WayfarerBootStep, theRead-Hostproxy, cwd-via-OSC-2), then launches pwsh with-NoProfile(so$PROFILEnever loads) and-Commanddot-sources that file directly. SeeConfiguration/PowerShellProfileProvisioner.cs. - Linux/macOS (bash/zsh): generates
~/.config/wayfarer-console/Wayfarer.Integration.shand~/.config/wayfarer-console/zsh/.zshrc. bash is launched with--rcfilepointed straight at the.shfile (replacing~/.bashrcoutright for that session); zsh has no such flag, so it's launched with$ZDOTDIRset to thatzsh/directory instead, redirecting zsh's own.zshrcfilename lookup there. bash reconstructs the same before/after-each-command hook from aDEBUGtrap +PROMPT_COMMAND; zsh uses its nativepreexec/precmdhooks. SeeConfiguration/NativeShellIntegrationProvisioner.cs. Any other shell (fish, dash, …) just gets its own ordinary startup — no markers, but the shell still starts and works fine.
The shell still starts immediately/concurrently with the boot splash either way (see Animated boot splash above) — none of this adds startup lag, since Wayfarer's own integration file is tiny compared to an arbitrary real profile. None of it is required for the shell to start, either — it's optional integration, degrading gracefully if provisioning fails (falls back to the shell's own ordinary startup) — but you shouldn't need to hand-copy anything for it to work.
packaging/example-profile.ps1 is the same
pwsh protocol as a human-readable reference/manual-install copy, for anyone
who'd rather wire this into their own real $PROFILE by hand instead.
Like the notification markers above, a checks entry with a step name
waits on a private OSC marker your shell integration emits — same wire
protocol, different type. The examples below are the pwsh side; the
bash/zsh equivalent is the same __wayfarer_marker/__wayfarer_bootstep
functions from Configuration/NativeShellIntegrationProvisioner.cs. Add
this alongside your existing Send-WayfarerMarker:
function Send-WayfarerBootStep {
param([Parameter(Mandatory)][string]$Name)
if ($env:WAYFARER_CONSOLE -eq '1') {
$b64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($Name))
Write-Host -NoNewline "$([char]27)]9731;bootstep;$b64$([char]7)"
}
}
Call it once a real piece of work finishes, with a -Name matching the
manifest check's step:
# ... slow module import, dotfile sourcing, whatever actually takes time ...
Send-WayfarerBootStep -Name "lxc-sync"
And once at the very end of $PROFILE (or the bash/zsh integration script),
regardless of whether any check names it explicitly — this is the
safety-net gate the splash always waits on before handing off to the
terminal:
Send-WayfarerBootStep -Name "ready"
If a named step never arrives (unwired shell integration, or an older one without this call), its bar falls back to simulated pacing after an ~8s timeout instead of hanging the splash indefinitely.
Sound assets
Assets/Sounds/pipboy-static.wav and Assets/Sounds/starware-terminal.wav
are placeholders, procedurally synthesized (filtered noise for PipBoy,
a clean sine-tone beep sequence for Starware) rather than sourced from any
real recording. This project is personal-use and not distributed, so
licensing real reference audio (e.g. an actual game's terminal sound) isn't
actually blocked the way it would be for a public release — it's simply
deprioritized indefinitely, not an open task being tracked toward. To
replace either anyway: drop a .wav at the same path with the same
filename (referenced by each manifest's bootStyle.sound), or point
bootStyle.sound at a new filename entirely.
Installing
Windows
Download a release zip, extract it anywhere, then run Install.ps1 from
inside the extracted folder:
powershell -ExecutionPolicy Bypass -File .\Install.ps1
This copies the app to %LocalAppData%\Programs\WayfarerConsole and adds a
Start Menu shortcut. Windows will likely flag the script as downloaded from
the internet (SmartScreen / "Windows protected your PC", or a blocked-file
warning) since it isn't code-signed — click "More info > Run anyway", or
run Unblock-File .\Install.ps1 first if PowerShell refuses to run it at
all. To remove it later, run Uninstall.ps1 (a copy is left in the install
directory) — it deletes the Start Menu shortcut and the installed folder.
Both scripts live in packaging/ in this repo and are bundled into every
release zip by the CI workflow.
Linux
Download the release tarball, extract it anywhere, then run install.sh
from inside the extracted folder:
tar -xzf Wayfarer-*-linux-x64.tar.gz -C wayfarer-console && cd wayfarer-console
./install.sh
This copies the app to ~/.local/share/WayfarerConsole, installs an icon
under ~/.local/share/icons, and registers a .desktop launcher entry
under ~/.local/share/applications so it shows up in your application
launcher. Spawns your own $SHELL (bash/zsh) — no PowerShell install
required on Linux. Desktop notifications go through org.freedesktop.Notifications
over D-Bus, so a session/notification daemon is expected (true of any normal
desktop session). Boot sound playback looks for paplay (PulseAudio/PipeWire)
or aplay (ALSA), falling back to silence if neither is present. Blur/acrylic
window transparency depends on your compositor and will fall back to a
solid background if unsupported (this is expected, not a bug). To remove
it later, run uninstall.sh (a copy is left in the install directory) — it
deletes the launcher entry, icon, and installed folder.
Both scripts live in packaging/ in this repo and are bundled into every
release tarball by the CI workflow.
Building
dotnet build WayfarerConsole/WayfarerConsole.csproj
Requires the .NET SDK (no Windows-only targeting needed — the app is cross-platform via Avalonia). To publish a self-contained single-file build for a specific platform:
dotnet publish WayfarerConsole/WayfarerConsole.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true
dotnet publish WayfarerConsole/WayfarerConsole.csproj -c Release -r linux-x64 --self-contained true -p:PublishSingleFile=true
Pushing a v*.*.* tag triggers the release workflow, which publishes both
self-contained win-x64 and linux-x64 builds and attaches the zip/tarball
to the corresponding release.
Architecture
- Pty engine: Porta.Pty — cross-platform pty spawning (ConPTY on Windows, a real pty on Linux), exposes read/write streams.
- VT parsing: VtNetCore — consumes the raw pty output and maintains a screen buffer (rows/cells/cursor/colors/attributes/scrollback viewport). Rendering is not its job — that's ours.
- Rendering/UI: Avalonia, a custom
DrawingContext-based painter (Controls/TerminalCanvas) that reads VtNetCore's screen buffer each frame, bypassing any built-in text control for full styling control and performance. - Input: Avalonia
KeyDown/TextInputevents translated to the byte sequences the shell's line editor (PSReadLine, readline, ZLE) expects and written to the pty's input stream. Pointer events drive mouse selection (Controls/TerminalCanvasowns the selection state/hit-testing/highlight;MainWindowwires the actual pointer/keyboard events and clipboard calls, same split as scrollback'sScrollBy/ScrollToBottom).
WayfarerConsole/
Assets/
Fonts/, Icons/ embedded Nerd Font + app icons
Sounds/ boot sound clips (placeholders -- see Sound assets above)
Boot/ boot splash sequencing (BootSplashRunner, BootStepSignal,
StaticNoiseText)
Configuration/ AppConfig/ConfigLoader/ManifestDefinition/BootCheckDefinition/
BootStyleDefinition/CursorSettings/BackgroundSettings/
BackgroundApplier
Controls/ TerminalCanvas (custom renderer + selection),
OrbitBackground (solar-system diagram, see Features above)
Effects/ ITerminalEffect + CRT/glow implementations + registry
Notifications/ marker scanner (notifications + boot-step markers),
popup window, stacking/anchoring manager
Rendering/ font resolution
Terminal/ PtySession (pty wrapper), VtNetCore controller/theme glue
Theme/
Manifests/ pipboy.json, starware.json — one file per persona
Presets/ standalone palettes that replace a manifest's palette when active
DefaultProfile.json template a brand-new operator profile is seeded from
(app.config.json lives per-user instead -- see Configuration above,
not bundled here)
packaging/
Install.ps1 copies a release to %LocalAppData%, adds a Start Menu shortcut
Uninstall.ps1 removes both
install.sh copies a release to ~/.local/share, adds a .desktop launcher entry
uninstall.sh removes both
wayfarer.png app icon for the Linux .desktop entry (ico isn't usable there)
Status / what's left
See docs/wayfarer-console-plan.md for the
milestone-by-milestone build log through the original WPF-era Milestone 9
(manifest-based theming), including bugs found and fixed along the way —
everything after that (the Avalonia rewrite it itself predicted, CI/installers,
the cinematic first-run recovery flow, the decorative background, cursor/
background/operator-name personalization, and mouse selection/clipboard)
isn't tracked milestone-by-milestone in that doc; see this README's Features
above, docs/new-new-features.md for the
theming/boot era specifically, or git log for the exact history. Short
version — done: pty wiring, custom rendering, boot splash (now tied to real
shell-integration progress, with per-persona presentation and sound),
manifest-based theming with 7 curated presets, a cinematic corrupted-profile
first-run/recovery flow, cursor/background/operator-name personalization,
notifications, the CRT/glow effects system, a decorative orbit background
(ambient + real Keplerian-mechanics modes), mouse selection and clipboard,
a WPF → Avalonia rewrite for cross-platform (Windows + Linux) support, and
CI-built releases with an installer/uninstaller script for both platforms.
Recently resolved (believed fixed, not re-verified further): a Linux
bug where keyboard input was completely dead when the installed build was
launched from the .desktop entry on Bazzite/KDE. Diagnostic instrumentation
added while chasing it (Diagnostics/InputDiagnostics.cs, on by default on
Linux, logs to input-diag.log next to the user's config) is intentionally
left in place rather than ripped out — cheap insurance if it resurfaces,
fail-silent by design so it can't itself cause a regression. Not being
actively worked further for now.
Deferred (all of the below, indefinitely — not currently being worked toward, revisit later if priorities change):
- Real, licensed sound assets to replace the two synthesized placeholders (see Sound assets above) — moot for now anyway, since this build is personal-use only and not distributed, so there's no actual licensing exposure driving urgency here.
- More effects beyond
crt/glow(the registry is built to make adding new ones cheap). - Windows 11's
ITerminalHandoffregistration (to appear in Settings > Apps > Default apps > Terminal alongside Windows Terminal) — investigated and deliberately deferred, not just unstarted. It needs package identity (a signed MSIX/sparse package) and a second terminal-hosting code path (attaching to console handles Windows hands you, instead of spawning your own pty), on top of an interface that's already had three breaking revisions. See Milestone 8 in the plan doc for the full findings — this is a scope change, not an incremental feature. - The remaining terminal-parity/functional backlog in
docs/new-new-features.mdsections 0.5/6 (tabs, split panes, session persistence, mouse reporting, OSC 8 hyperlinks, clickable URLs, vi-style copy mode, ligature support, etc.) and the orbit-background expansion backlog indocs/new-features.md.
Explicitly out of scope for v1: tabs/multiple panes, SSH/WSL profiles (PowerShell-only), full xterm compliance beyond what VtNetCore handles, and window-manager-level blur/acrylic on Linux setups without compositor support (falls back to a solid background instead).