No description
  • C# 97.1%
  • PowerShell 2.1%
  • Shell 0.8%
Find a file
2026-07-23 17:19:44 -04:00
.claude/skills/verify Updated docs 2026-07-21 15:30:48 -04:00
.forgejo/workflows M8: Linux packaging and CI 2026-07-13 09:51:51 -04:00
.idea/.idea.WayfarerConsole/.idea Baseline: WPF Wayfarer Console before Avalonia/Linux rewrite 2026-07-12 17:03:26 -04:00
docs Added tab support and fixed the bug of ghost text hanging around after backspacing 2026-07-23 17:01:43 -04:00
packaging fixed the issue of NO FUCKING PROFILE BEING MADE DESPITE THAT BEING THE FUCKING FEATURE REQUESTED 2026-07-13 20:00:52 -04:00
WayfarerConsole fixed the help options 2026-07-23 17:19:44 -04:00
.gitattributes Enforce LF line endings for shell scripts 2026-07-13 09:52:29 -04:00
.gitignore M0: swap WPF scaffold for Avalonia 2026-07-12 17:12:28 -04:00
README.md Added tab support and fixed the bug of ghost text hanging around after backspacing 2026-07-23 17:01:43 -04:00
WayfarerConsole.sln Baseline: WPF Wayfarer Console before Avalonia/Linux rewrite 2026-07-12 17:03:26 -04:00

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

  1. Download the latest release .zip.
  2. Run Install.ps1 in a PowerShell terminal:
    powershell -ExecutionPolicy Bypass -File .\Install.ps1
    

Linux

  1. Download the latest release .tar.gz.
  2. 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 $SHELL on 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-grid DrawingContext renderer (not a TextBox), 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 no step stay 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 via bootStyle in 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 directional wipe). 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 via activeManifest in your per-user app.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: glow only). 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 :help for the full command list, including :orbit/:orbitspeed below.
  • 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.Notifications over 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 shadowed read on 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-WayfarerMarker on prompt in pwsh's case, the equivalent __wayfarer_marker in bash/zsh's case; a Read-Host/read proxy 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 bootSoundEnabled regardless of what the active manifest opts into. See Sound assets below.
  • Mouse-wheel scrollback, using VtNetCore's screen-buffer viewport directly (no ScrollViewer involved).
  • 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+V copy/paste (plain Ctrl+C/Ctrl+V stay 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 :orbit command; :orbitspeed swaps 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-x64 and linux-x64 single-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 under Theme/Manifests/ to load (currently pipboy or starware).
  • activePreset — id of a Theme/Presets/{id}.json whose palette/font/cursor replaces the active manifest's own wholesale, or null to 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's bootStyle.sound opts 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, plus Vintage/EmptyBox/ DoubleUnderscore with 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 under Assets/Sounds/, or omit/null for 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, the Read-Host proxy, cwd-via-OSC-2), then launches pwsh with -NoProfile (so $PROFILE never loads) and -Command dot-sources that file directly. See Configuration/PowerShellProfileProvisioner.cs.
  • Linux/macOS (bash/zsh): generates ~/.config/wayfarer-console/Wayfarer.Integration.sh and ~/.config/wayfarer-console/zsh/.zshrc. bash is launched with --rcfile pointed straight at the .sh file (replacing ~/.bashrc outright for that session); zsh has no such flag, so it's launched with $ZDOTDIR set to that zsh/ directory instead, redirecting zsh's own .zshrc filename lookup there. bash reconstructs the same before/after-each-command hook from a DEBUG trap + PROMPT_COMMAND; zsh uses its native preexec/precmd hooks. See Configuration/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/TextInput events 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/TerminalCanvas owns the selection state/hit-testing/highlight; MainWindow wires the actual pointer/keyboard events and clipboard calls, same split as scrollback's ScrollBy/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 ITerminalHandoff registration (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.md sections 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 in docs/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).