# SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie # SPDX-License-Identifier: MIT { description = "A reference for terminal control sequences, with every example run live in libghostty-vt"; inputs = { nixpkgs = { url = "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"; }; zig = { url = "git+https://git.jcollie.dev/jeff/zig-overlay.git"; inputs.nixpkgs.follows = "nixpkgs"; }; zon2nix = { url = "github:jcollie/zon2nix/zig-0.17"; inputs.nixpkgs.follows = "nixpkgs"; }; # Only the source is wanted: the wasm is built by ghostty-vt-wasm.nix. ghostty = { url = "github:ghostty-org/ghostty"; flake = false; }; }; outputs = { self, nixpkgs, zig, zon2nix, ghostty, ... }: let inherit (nixpkgs) lib; makePackages = system: import nixpkgs { inherit system; }; forAllSystems = lib.genAttrs lib.systems.flakeExposed; # Ghostty and Zine both build with the released 0.17.0. zigFor = system: zig.packages.${system}."0.17.0"; ghosttyRevision = ghostty.shortRev or "dirty"; wasmFor = pkgs: pkgs.callPackage ./ghostty-vt-wasm.nix { inherit ghostty; zig_0_17 = zigFor pkgs.stdenv.hostPlatform.system; revision = ghosttyRevision; }; mermaidFor = pkgs: pkgs.callPackage ./mermaid.nix { }; in { packages = forAllSystems ( system: let pkgs = makePackages system; ghostty-vt-wasm = wasmFor pkgs; mermaid = mermaidFor pkgs; in rec { inherit ghostty-vt-wasm mermaid; website = pkgs.callPackage ./website.nix { zig_0_17 = zigFor system; inherit ghostty-vt-wasm mermaid; }; default = website; } ); # The site's cases run in real terminals, in NixOS virtual machines. # Linux only: a NixOS test cannot be evaluated for Darwin. checks = lib.genAttrs (lib.filter (lib.hasSuffix "-linux") lib.systems.flakeExposed) ( system: let pkgs = makePackages system; harness = pkgs.callPackage ./emulators/harness.nix { }; emulator = args: pkgs.testers.runNixOSTest ( import ./emulators/vm.nix (args // { inherit harness; }) ); # xterm with its default resources but one: GetChecksum off the # disallowed list, so that DECRQCRA answers (see case.mjs). A case # with the no-column-resize host setting also gets ColumnMode on # it, so that DECCOLM does not change the width (see run.mjs). xtermCommand = xterm: [ "${xterm}/bin/xterm" "-xrm" "*disallowedWindowOps: GetIconTitle,GetWinTitle,SetSelection,GetSelection,SetXprop{host:no-column-resize:,ColumnMode}" "-name" "{name}" "-geometry" "{cols}x{rows}" "-T" "{title}" "-e" ]; xtermDefault = pkgs.callPackage ./emulators/xterm-default.nix { }; # rxvt-unicode 9.31 defines its own `lerp`, which a compiler that # defaults to C++20 finds ambiguous with std::lerp; it builds as # C++17, which changes nothing about how it behaves. urxvtPackage = pkgs.rxvt-unicode-unwrapped.overrideAttrs (old: { env = (old.env or { }) // { NIX_CFLAGS_COMPILE = toString (old.env.NIX_CFLAGS_COMPILE or "") + " -std=gnu++17"; }; }); # A default xterm's colors, so that color answers can be compared # with the cases, which follow one. xtermColors = lib.concatLists ( lib.imap0 (i: c: [ "-o" "color${toString i}=#${c}" ]) [ "000000" "cd0000" "00cd00" "cdcd00" "0000ee" "cd00cd" "00cdcd" "e5e5e5" "7f7f7f" "ff0000" "00ff00" "ffff00" "5c5cff" "ff00ff" "00ffff" "ffffff" ] ); in { xterm-default = emulator { name = "xterm-default"; terminal = "xterm"; package = xtermDefault; profile = "xterm"; command = xtermCommand xtermDefault; }; xterm = emulator { name = "xterm"; terminal = "xterm"; package = pkgs.xterm; profile = "xterm"; command = xtermCommand pkgs.xterm; }; # pyte is a library, so it needs no VM: one Python process runs # every case (see emulators/pyte.mjs). pyte = let python = pkgs.python3.withPackages (p: [ p.pyte ]); src = lib.fileset.toSource { root = ./.; fileset = lib.fileset.unions [ ./emulators/library.mjs ./emulators/pyte.mjs ./emulators/pyte_driver.py ./tools/cases.mjs ./website/assets/vtcase.js ./website/content ]; }; in pkgs.runCommand "cc-pyte" { nativeBuildInputs = [ pkgs.nodejs python ]; } '' mkdir -p $out node ${src}/emulators/pyte.mjs ${src}/website/content $out/results.json ''; # xterm.js, headless, runs in-process in Node like pyte (see # emulators/xtermjs.mjs). The npm package has no dependencies, so # its tarball is all there is to fetch. xtermjs = let headless = pkgs.fetchzip { url = "https://registry.npmjs.org/@xterm/headless/-/headless-6.0.0.tgz"; hash = "sha256-Kh/i9Xlex3+nHMqDa2Voz1vNAsm2i9aDFjIxPQZqwic="; }; src = lib.fileset.toSource { root = ./.; fileset = lib.fileset.unions [ ./emulators/library.mjs ./emulators/xtermjs.mjs ./emulators/kitty-screen.mjs ./tools/cases.mjs ./website/assets/vtcase.js ./website/content ]; }; in pkgs.runCommand "cc-xtermjs" { nativeBuildInputs = [ pkgs.nodejs ]; } '' mkdir -p $out CC_XTERMJS=${headless} node ${src}/emulators/xtermjs.mjs ${src}/website/content $out/results.json ''; # alacritty_terminal, the crate Alacritty is built on, through a # small driver of ours (see emulators/alacritty.mjs). No VM: the # crate is a library. alacritty = let driver = pkgs.rustPlatform.buildRustPackage { pname = "cc-alacritty"; version = "0.0.0"; src = ./emulators/alacritty; cargoLock.lockFile = ./emulators/alacritty/Cargo.lock; }; src = lib.fileset.toSource { root = ./.; fileset = lib.fileset.unions [ ./emulators/library.mjs ./emulators/alacritty.mjs ./tools/cases.mjs ./website/assets/vtcase.js ./website/content ]; }; in pkgs.runCommand "cc-alacritty-results" { nativeBuildInputs = [ pkgs.nodejs ]; } '' mkdir -p $out node ${src}/emulators/alacritty.mjs ${driver}/bin/cc-alacritty ${src}/website/content $out/results.json ''; # foot is Wayland-only, so it runs under a headless sway. It has # no way to be asked for its screen but a key binding: pipe-visible, # bound to Ctrl+Shift+F9 and pressed by the harness with wtype, # writes the visible text where case.mjs reads it. foot = let pipe = pkgs.writeShellScript "cc-foot-pipe" '' cat > /tmp/cc-foot-screen.tmp && mv /tmp/cc-foot-screen.tmp /tmp/cc-foot-screen ''; palette = [ "000000" "cd0000" "00cd00" "cdcd00" "0000ee" "cd00cd" "00cdcd" "e5e5e5" "7f7f7f" "ff0000" "00ff00" "ffff00" "5c5cff" "ff00ff" "00ffff" "ffffff" ]; colors = lib.concatLists ( lib.imap0 ( i: c: [ "-o" "colors-dark.${if i < 8 then "regular" else "bright"}${toString (lib.mod i 8)}=${c}" ] ) palette ); in emulator { name = "foot"; terminal = "foot"; package = pkgs.foot; profile = "foot"; display = "sway"; command = [ "${pkgs.foot}/bin/foot" "--app-id" "{name}" "--title" "{title}" "--window-size-chars" "{cols}x{rows}" "-o" "pad=0x0" "-o" "csd.preferred=none" "-o" "colors-dark.foreground=000000" "-o" "colors-dark.background=ffffff" "-o" "colors-dark.cursor=ffffff 000000" "-o" "colors-dark.selection-foreground=ffffff" "-o" "colors-dark.selection-background=000000" "-o" "key-bindings.pipe-visible=[${pipe}] Control+Shift+F9" ] ++ colors; extraConfig = { fonts.packages = [ pkgs.dejavu_fonts ]; }; }; # VTE, the widget GNOME Terminal is built on, through PyGObject. # It is a library, so no VM; but it is a GTK widget, so it needs a # display, and Xvfb is one (see emulators/vte.mjs). vte = let python = pkgs.python3.withPackages (p: [ p.pygobject3 ]); # The typelibs are in each package's out output, which is # not always its first. typelibs = lib.makeSearchPathOutput "out" "lib/girepository-1.0" [ pkgs.vte pkgs.gtk3 pkgs.pango pkgs.gdk-pixbuf pkgs.at-spi2-core pkgs.harfbuzz pkgs.glib pkgs.gobject-introspection ]; fonts = pkgs.makeFontsConf { fontDirectories = [ pkgs.dejavu_fonts ]; }; src = lib.fileset.toSource { root = ./.; fileset = lib.fileset.unions [ ./emulators/library.mjs ./emulators/vte.mjs ./emulators/vte_driver.py ./tools/cases.mjs ./website/assets/vtcase.js ./website/content ]; }; in pkgs.runCommand "cc-vte" { nativeBuildInputs = [ pkgs.nodejs python pkgs.xvfb-run ]; } '' export HOME=$TMPDIR XDG_RUNTIME_DIR=$TMPDIR export GI_TYPELIB_PATH=${typelibs} FONTCONFIG_FILE=${fonts} mkdir -p $out xvfb-run -a node ${src}/emulators/vte.mjs ${src}/website/content $out/results.json ''; # rxvt-unicode, with nothing loaded but the harness's own Perl # extension, which is how its screen is read back (see case.mjs), # and a default xterm's colors. urxvt = emulator { name = "urxvt"; terminal = "rxvt-unicode"; package = urxvtPackage; profile = "urxvt"; command = [ "${urxvtPackage}/bin/urxvt" "-name" "{name}" "-title" "{title}" "-geometry" "{cols}x{rows}" "-xrm" "URxvt.perl-ext-common:" "-perl-lib" "${./emulators/urxvt}" "-pe" "cc-harness" "-fg" "#000000" "-bg" "#ffffff" "-cr" "#000000" ] ++ lib.concatLists ( lib.imap0 (i: c: [ "-xrm" "URxvt.color${toString i}: #${c}" ]) [ "000000" "cd0000" "00cd00" "cdcd00" "0000ee" "cd00cd" "00cdcd" "e5e5e5" "7f7f7f" "ff0000" "00ff00" "ffff00" "5c5cff" "ff00ff" "00ffff" "ffffff" ] ) ++ [ "-e" ]; }; kitty = emulator { name = "kitty"; terminal = "kitty"; package = pkgs.kitty; profile = "kitty"; command = [ "${pkgs.kitty}/bin/kitty" "--class" "{name}" "--name" "{name}" "--listen-on" "unix:/tmp/cc-kitty" "-o" "allow_remote_control=yes" "-o" "remember_window_size=no" "-o" "initial_window_width={cols}c" "-o" "initial_window_height={rows}c" "-o" "window_padding_width=0" "-o" "placement_strategy=top-left" "-o" "enable_audio_bell=no" "-o" "shell_integration=disabled" "-o" "confirm_os_window_close=0" "-o" "foreground=#000000" "-o" "background=#ffffff" "-o" "cursor=#000000" "-o" "selection_foreground=#ffffff" "-o" "selection_background=#000000" ] ++ xtermColors; extraConfig = { hardware.graphics.enable = true; fonts.packages = [ pkgs.dejavu_fonts ]; }; }; } ); devShells = forAllSystems ( system: let pkgs = makePackages system; ghostty-vt-wasm = wasmFor pkgs; in { default = pkgs.mkShell { name = "control-codes"; nativeBuildInputs = [ (zigFor system) zon2nix.packages.${system}.zon2nix pkgs.git-pages-cli pkgs.nodejs pkgs.reuse ]; # `zig build serve -Dvt-wasm="$VT_WASM" -Dmermaid="$MERMAID_JS"` # for local work. VT_WASM = "${ghostty-vt-wasm}/bin/ghostty-vt.wasm"; MERMAID_JS = "${mermaidFor pkgs}/mermaid.min.js"; GHOSTTY_REVISION = ghosttyRevision; }; } ); }; }