harrier #
A GPU terminal emulator in Zig 0.17. The terminal itself — parsing, the screen, scrollback, modes, key and mouse encoding — is libghostty-vt. harrier is everything around it: windows of tabs and split panes, and a program on a pseudo-terminal or a serial port at the other end. It draws them with zig-aerie — the window system, Vulkan renderer and font stack that began as harrier's own and is now a library other programs draw with too.
$ harrier # your shell
$ harrier -e htop # a program
$ harrier --serial /dev/ttyUSB0 --baud 9600 # a serial port
One harrier serves every window. A launch with a harrier already running hands its window to that one and exits, so a new window opens at once. Where the systemd user service is installed, the session bus starts harrier for the first window too; see Running as a service.
It is new, and runs on Linux under Wayland, and on Windows 10 1809 and later, where some of what follows is not there yet; see Windows.
What it does #
-
The terminal is Ghostty's. It uses libghostty-vt from the
zig-0.17-jeffbranch of my fork of Ghostty: Ghostty'szig-0.17branch (pull request #14519) with my work not yet merged upstream on top. That is xterm and kitty keyboard and mouse encoding, synchronized output, clipboard reads and writes with OSC 52 and with kitty's clipboard protocol (OSC 5522) and its paste events, titles, scrollback, size reports down toCSI 19 t, and the rest of what Ghostty's own terminal does. -
A pty or a serial port. The window shows whatever the backend is connected to. A program runs on a pseudo-terminal with
TERM,COLORTERM=truecolorandTERM_PROGRAM=harrierset. A serial port is opened raw through zig-serial, at any rate the driver accepts. -
Wayland, without libwayland. The window and its input go through zig-aerie's platform, on zig-wayland-native and zig-xkb:
- fractional scaling;
- server-side decorations;
- key repeat;
- dead keys and the Compose key;
- the clipboard, and the primary selection: select to copy, middle-click to paste, and a toast over the pane saying what was copied, or what a program did with the clipboard without asking;
- input methods through text-input-v3;
- cursor shapes;
- its own title bar and frame where the compositor draws none — the
title in the desktop's interface font, as other windows have it — with
the focused pane's location under the title: the directory its program is
in, and the machine when the shell is on another one, over ssh, say
(see
window.subtitle); - resizing in whole cells: a floating window with one pane takes, of each size the compositor offers, the largest that fits a whole number of cells, so no strip of a cell is left empty along the right or bottom edge. A maximized, fullscreen or tiled window has to take the size it is given, and a window split into panes leaves each pane's own strip.
-
Vulkan, through dma-bufs, by zig-aerie's renderer. Every frame is instanced quads drawn from a glyph atlas and the images programs show. There is no
wl_displayfor a Vulkan swapchain, so zig-aerie does what a swapchain does itself: it allocates exportable images the compositor reads directly, in a layout (DRM format modifier) both sides share. The frame is drawn straight into one of these.- Without modifier support (RADV on Polaris and older, say), there is no image both can use. The buffer is then a linear dma-buf, and the frame is copied into it on the GPU.
- Sync is explicit (linux-drm-syncobj) where the compositor offers it, and implicit otherwise.
- Where no memory can be shared at all, the frame is read back into shared memory.
-
Jeff's font stack, through zig-aerie.
-
Fonts are found the way fontconfig finds them, by zig-font-config.
-
Text is shaped by zig-font-shaper, a port of HarfBuzz, so ligatures and combining marks work. Only rows that changed are shaped again.
-
Glyphs are drawn by zig-font-renderer, color ones too:
COLR,SVG, and the bitmap emoji ofCBDTandsbix. -
Emoji are drawn in color. A character Unicode shows as an emoji (UTS #51's Emoji_Presentation), or one followed by U+FE0F, is drawn from the first font in the fallback order that has it in color, even when fontconfig ranks a monochrome emoji font first; a character shown as text, such as ❤ alone or 😐 followed by U+FE0E, is drawn from the first that has it as an outline. A selector Unicode does not define for the character before it, as U+FE0E is not for 😀, is ignored, except under
grapheme_width = .legacy. The property is read at compile time from the Unicode data the shaper's uucode carries. -
A grapheme takes the cells Unicode says: ➡️, ➡ followed by U+FE0F, is wide like any other emoji, and a flag or a family is one grapheme of two cells, with mode 2027 on by default. A color glyph that is still wider than its cells, as an emoji is in a narrow cell under
grapheme_width = .legacy, is shrunk to fit them rather than covering the next. Text harrier draws itself in cells, in tabs and the text an input method is composing, is measured the same way. -
Some glyphs are drawn rather than taken from the font, the way Ghostty does it, with Ghostty's own drawing code:
- box drawing, block elements and shades;
- braille;
- Powerline's separators and branch symbols;
- geometric shapes;
- the symbols for legacy computing (sextants, octants, wedges).
Drawn to the cell, lines meet their neighbours without gaps and braille dots fill the cell, whatever the font's own versions look like.
-
-
Tabs and splits. A window holds tabs, with a strip of them above once there are two, and a tab holds panes, split side by side or one above another as often as is wanted. New tabs and panes start in the directory the focused pane is in.
-
Drop files to type their paths. Dropped on a pane whose program takes no drops, files are typed in as their paths, quoted for a shell and separated by spaces, and dropped text is typed as it is. Nothing is followed by a newline, so a drop never runs anything, and a drop is always a copy, never a move.
-
Drag and drop for the programs in it. A program speaking kitty's drag and drop protocol (OSC 72), like
kitten dnd, can take what is dragged onto its pane, from another program or another pane, and drag things out of its pane to anywhere that takes drops. A program on another machine, over ssh, works too: it is sent the files dropped on it, and the files it drags are written under~/.cache/harrier/drags(on Windows,%LOCALAPPDATA%\harrier\drags) and handed on from there.drag_and_drop = falseturns the protocol off. -
Images. A program drawing with kitty's graphics protocol, like
kitten icat, can show pictures in its pane:- RGB, RGBA and PNG, the PNGs decoded with Wuffs;
- sent in the escape sequences themselves, or, from a program on this machine, as a file, a temporary file or shared memory;
- cropped and scaled to the cells asked for, and moving with the text as it scrolls;
- drawn over the text, under it, or under the cells' backgrounds too, as each placement's z-index asks;
- placed by unicode placeholders, which survive tmux;
- animated.
images.enabled = falseturns them off. -
Searching the scrollback.
ctrl+shift+fopens a search field at the top right of the focused pane, and what is typed is looked for through the whole of its terminal: the screen and every line of scrollback, following the program onto the alternate screen and back, and keeping up as output arrives. Every match in view is highlighted, and the count says how many there are; Enter goes to the next older match, scrolling to it, and Shift+Enter (or the arrow keys) back down. Escape closes the field, and the search is kept:ctrl+shift+gandctrl+shift+hgo on to the next older and newer match with the field closed, showing only the one they land on, until something is typed to the program. With nothing searched for yet they open the field, andctrl+shift+fopens it again on the last search. While it is open, pastes go into it rather than to the program —ctrl+shift+v,shift+insertand the middle button alike — made one line, each newline or tab a space. A long scrollback is searched a piece at a time between frames, so the window stays responsive and the count fills in as it goes. -
Profiles.
ctrl+shift+p, or the▾button in the title bar or the tab bar, opens the profile selector, which lists the hosts, serial consoles and programs the configuration names as profiles, and opens the one picked in a window of its own, with its own environment and colors. See Profiles. -
Links open with ctrl+click. A URL in the text, or a hyperlink a program makes (OSC 8), is underlined while ctrl is held over it, and ctrl+click opens it with whatever the desktop opens it with, through the XDG desktop portal: a web page in the browser, a file on this machine in its application. This works even when the program has the mouse.
link_detection = falseleaves plain-text URLs alone. -
Desktop notifications. A program asking for one with OSC 9 or OSC 777 — a long build when it finishes, say — gets one through the XDG desktop portal, titled with its tab's title when it gives none. Clicking it shows that tab and pane, raising the window, and looking at the pane withdraws it. By default a pane being looked at sends none.
-
The bell. A program ringing its bell (BEL) in a pane you are not looking at marks that pane's tab and the window's title with 🔔 until you look, and asks the desktop for attention when the window does not have the focus: "is ready" on GNOME and KDE, an urgent border on sway, a flashing taskbar button on Windows. A sound — the desktop's own bell, or a sound file of your choosing played by zig-chime, for all panes or a profile's — a flash of the pane and a desktop notification can be turned on too; see
bellunder Configuration. -
Progress bars. A program reporting its progress with OSC 9;4, as ConEmu and Windows Terminal take it, gets a thin bar along the top of its pane: filled as far as it says, in its palette's blue, red for an error and yellow when paused, or a stretch sweeping across when it cannot say how far it is — colors the configuration can change. Its tab on the tab bar shows the same along its bottom edge, so that progress in a hidden tab can be seen: an error in any of the tab's panes first, then the focused pane's progress, then any other pane's. The bar goes when the program says it is done, or after fifteen seconds without word from it. A dock or task manager that reads Unity's launcher API — KDE Plasma's, Dash to Dock, Dash to Panel — shows the progress on harrier's icon too: the mean of every pane that says how far it is, with the icon marked urgent while one reports an error or is waiting on the user.
-
Program status. A program that says what it is doing with OSC 7501 — working, waiting on the user, done, failed, idle — keeps a record for itself and for each piece of work it names, in a tree:
deploy/us-eastbeneathdeploy. Coming into working, waiting or done, a record sends a desktop notification, titled with its title or its program, saying what happened and the program's message; a pane sends at most four in ten seconds. Its pane's progress bar follows the most recent report: green when done, yellow while waiting. Once a program has reported with OSC 7501, its OSC 9;4 progress is set aside until a full reset, as the protocol asks. What was going on ends when the program exits or the shell shows its next prompt; what is done or failed stays until the program clears it. A pane keeps 256 records; a new one past that takes the place of the one reported least recently.A status button is always shown. While anything is reported, it is the most pressing report's mark, in its color, and how many there are, such as
✗ 3when something has failed; with nothing reported it is an empty circle, greyed out. It sits in harrier's title bar when harrier draws one, otherwise in the tab bar when that shows, and otherwise as a small badge at the top right of the terminal. Pressing it, orctrl+shift+s, opens the status window: every terminal's progress and records as a tree (windows, tabs, panes and the records beneath each) in a window of its own. It is a child of the window it was opened from, so most compositors float it and keep it above that window. It can be moved aside and left open while working, and it follows the reports as they change. A click on a row shows that pane, and Escape closes it. -
A cgroup for each program. Each terminal's program runs in a systemd scope of its own,
app-dev.jcollie.harrier-<pid>.scope, made before it starts, so that it and everything it starts are accounted for, limited, and killed under memory pressure apart from harrier and from the other terminals: a runaway build in one tab is not the whole terminal. -
One process, many windows. The first harrier owns
dev.jcollie.harrieron the session bus and servesorg.freedesktop.Application. Later launches hand their windows to it with their command, working directory and environment, and launchers use the same interface through the desktop entry'sDBusActivatable=true. Windows opened this way take focus through xdg-activation. -
Found from GNOME Shell's search. Typing in the overview finds harrier's open panes by title and location, and picking one shows it, and its profiles, which picking starts.
-
A terminal for other programs to launch in. harrier implements the Terminal Intent,
org.freedesktop.Terminal1, so a launcher can run a program whose desktop file saysTerminal=truein it without knowing harrier's command line. -
Configured in struthio. Its programs can ask which machine they are running on.
-
Ready to be translated. What harrier itself says goes through Project Fluent, by zig-fluent. English is the only translation so far; see Translations.
Building and running #
$ nix develop
$ zig build run # build, then run your shell
$ zig build run -- -e vim README.md
$ zig build test --summary all
The development shell has Zig 0.17.0, the Vulkan loader and its validation
layer, and REUSE. --validation turns the layer on. zig-aerie's shaders
are Zig, compiled by Zig's own SPIR-V backend, so the build needs nothing
but Zig.
The font libraries are built ReleaseSafe even in a Debug build. Reading fontconfig's cached patterns takes seconds when they are unoptimized.
So is libghostty-vt. Built Debug, it checks a whole page's integrity after
every change to it, which turns output full of hyperlinks into minutes of a
core at full tilt — 3,000 hyperlinked lines take over two minutes rather
than a moment. -Dvt-debug builds it Debug anyway, to debug it.
The package and the VM test:
$ nix build # result/bin/harrier
$ nix build .#checks.x86_64-linux.sway # harrier in a NixOS VM
The package's Zig dependencies come from build.zig.zon.nix, which
zon2nix generates. After changing build.zig.zon, regenerate it:
$ nix develop -c tools/zon2nix.sh
It leaves out the dependencies of Ghostty's own application (its fonts, images, GUI toolkits and shader compilers), which libghostty-vt never builds.
The VM test runs harrier in a headless sway, with lavapipe standing in for a GPU. pixman offers no dma-bufs, so this run presents through shared memory. It checks that the screenshot has text in it.
On Windows #
Zig is all it takes, and a GPU driver with Vulkan 1.2, which every current one has:
> zig build run
> zig build test --summary all
harrier is built for the GNU ABI, x86_64-windows-gnu, on Zig's own MinGW
headers, so no Visual Studio is needed; a Windows target that names no ABI
gets that one. Ghostty's build would otherwise take MSVC for itself.
The installer is an MSI, built by
zixl from
dist/windows/harrier.wxs as one more step of the build, on any machine
Zig runs on:
$ zig build msi -Dtarget=x86_64-windows-gnu
$ nix build .#msi
Either puts harrier-VERSION-x64.msi in zig-out or result, with its
ProductCode beside it in harrier-VERSION-x64.msi.ProductCode. The
build is reproducible: built with Nix, one commit makes one MSI, bit for
bit, and so one ProductCode.
Each release has one,
built when a tag v and the version in build.zig.zon is pushed. It
installs harrier for all users, in Program Files\harrier, with a Start
menu shortcut, the directory on the PATH, an Open in harrier item
in Explorer's menu for a folder, a drive and a folder's background (on
Windows 11, under Show more options), and harrier as a source in the
Event Log. An MSI replaces any harrier of its version or an older
one, removing it first; it will not install over a newer one.
Installing with winget #
Every release is also published to the JCOllie winget source, as
JCOllie.Harrier. Add the source once, from an elevated prompt:
> winget source add -n JCOllie -a https://winget.jcollie.dev/ -t Microsoft.PreIndexed.Package --trust-level trusted
then install harrier, and later upgrade it, as any other package:
> winget install JCOllie.Harrier -s JCOllie
> winget upgrade JCOllie.Harrier -s JCOllie
The MSI is not signed. winget marks an installer from an untrusted source
as downloaded from the internet, and Microsoft Defender SmartScreen then
stops an unsigned one with "Windows protected your PC". A source added with
--trust-level trusted skips that; one added without it can be removed
with winget source remove JCOllie and added again. An MSI downloaded from
the release page gets the same warning, and Run anyway installs it.
Windows #
harrier on Windows is the same terminal in a Win32 window, presenting through a Vulkan swapchain, with programs on a pseudoconsole (ConPTY). What is different:
-
The pseudoconsole is harrier's own: the
conpty.dllbeside harrier.exe and thex64\OpenConsole.exeit runs, from Microsoft's ConPTY package, the console host Windows Terminal ships. Windows' own keeps escape sequences it does not know, OSC 7501 among them, from harrier, and answers some queries in its place; it is used only when harrier's own is missing, and harrier logs a warning when it is. The console host counts a character's cells as the pane'sgrapheme_widthsays. -
The shell is PowerShell (
pwsh), or else Windows PowerShell, or elseCOMSPEC, as Windows Terminal chooses;commandand-erun anything else. -
Where a window starts is where
harrierwas run, as on Linux, but not the system directory,C:\Windows\System32, where Windows starts a harrier from the Start Menu, a pin or a jump list item: there, and wherever nothing says, it is the user's home,%USERPROFILE%. -
TERMisxterm-256colorby default: Windows has no terminfo to install harrier's entry in, andsshhandsTERMto the server. -
The title bar is harrier's own, as on a Linux desktop that leaves it to programs, with the profile selector's
▾and the status button, in a frame Windows draws: its shadow and rounded corners, the edges it resizes by, Aero Snap, and the snap layouts offered over the maximize button.window.title_bar = .serverhas Windows draw its own title bar instead, dark or light as programs are set to be in Settings, and.nonehas no title bar at all. harrier's title is in Segoe UI, as Windows' own are. The window menu — Alt+Space, or a right press on the title bar — also offers a new window, a new tab and the profile selector. Its subtitle shows a location only when the shell reports one with OSC 7, since nothing on Windows can ask another process where it is. -
The primary selection is harrier's own: selecting copies and the middle button pastes between its panes, but other programs do not see it.
-
One process, many windows, through a named pipe,
\\.\pipe\dev.jcollie.harrier-<SID>, open only to the user, rather than the session bus. A launch checks that whoever serves it is the same user before it sends its command and environment. -
As administrator, a program runs in a harrier of its own that is elevated, as in Windows Terminal: a program that is not elevated cannot start one that is in its own window.
harrier --elevate, or a profile with.elevate = true, starts harrier again through Windows' "Run as administrator", which asks first; that harrier owns the elevated windows, and later elevated launches open theirs in it. Each one from a program that is not elevated is asked about again, as in Windows Terminal: only Windows' prompt may lift it; one from an elevated shell is not. The elevated harrier serves a pipe of its own,…-<SID>-admin, labelled so that nothing below high integrity can connect and checked again for each launch, so a program that is not elevated can never have it run something. Its windows' titles start with "Administrator: ". Windows keeps Explorer, which is not elevated, from dropping files on them. -
Warnings and errors go to the Application log too, under the source
harrier, since a GUI program's standard error is seen only where it is redirected; one that stops harrier starting is also shown in a message box when there is nowhere else to say it. harrier.exe carries the message table Event Viewer shows them through, but naming it takes a key underHKEY_LOCAL_MACHINE, so the first harrier run as administrator writesSYSTEM\CurrentControlSet\Services\EventLog\Application\harrier; until then Event Viewer says it found no description before each message. -
Fonts are Windows' own and the user's, found by zig-font-config;
monospaceis Cascadia Mono, or Consolas where that is not installed.%APPDATA%\harrier\fonts.confis read as a fontconfig file, for aliases and fonts of your own. -
Configuration is
%APPDATA%\harrier\config.struthio. -
It is a GUI program, so that started from Explorer or a shortcut it opens no console window. What it writes —
--help,--version, its log, why it could not start — is seen only where standard output and error are redirected (harrier --help > help.txt), and not in the console it was started from. -
Input methods go through IMM32: what is being composed is drawn by harrier at the cursor, as on Wayland, and the input method's candidate window opens just below it.
-
Global keybinds are registered with
RegisterHotKey. -
Links open with whatever Windows has for them.
-
Serial ports are
COM3and the like, looked at every 10 ms rather than waited on. -
Programs run in a job object that ends them with their pane, as a hangup does on Linux.
-
PNGs that programs send are decoded by z2dimg, not wuffs: translate-c turns the fortified string functions of an optimized MinGW build into Zig that does not compile, and wuffs' header pulls them in.
-
Drops from other programs come through OLE: files dropped on a pane whose program takes no drops are typed as Windows paths, in double quotes when they need them, which suits cmd and PowerShell alike. A program taking drops gets the files as a list of
file:URIs, or the text; the program on the other side is always told it was a copy, since after a move it would delete what the program may not have read yet. -
Drags out of a program go through OLE too, as files when the program offers a text/uri-list of paths on this machine, and as text when it offers text. A drop target asks for the data the moment it is dropped, so harrier asks the program and waits for it, up to five seconds without word from it, turning its loop meanwhile.
-
A program on another machine, or under WSL, which counts as one, is sent the files dropped on it, and the files it drags are written under
%LOCALAPPDATA%\harrier\drags. A name Windows cannot hold — one with:or?in it, orCON— has_in place of what it cannot hold, and a symbolic link is left out, since making one takes a privilege the user may not have. A drop target waits for them all, however long they take, as long as they keep coming. -
The jump list — right-click harrier's taskbar button, or its pin in Start — lists the open panes, each by its title and location, and the profiles. Picking a pane shows it; picking a profile starts it. Windows has nothing like GNOME Shell's search provider: Start asks no running program what it has as the user types, so this is the nearest thing. Each pane's item runs
harrier --focus-pane PRIMARY:ID, which hands off to the harrier running. Windows keeps the list after harrier exits, so harrier takes the panes out of it as it does, and an item left by one that crashed brings forward the last window, or opens one, rather than showing another harrier's pane with the same ID. Only a harrier that is not elevated writes it; the elevated one's button has none. With Show recently opened items in Start, Jump Lists and File Explorer off, Windows takes no panes in it, only the profiles. The list is replaced at most once a second, however often titles change. -
The taskbar button is
dev.jcollie.harrier's, harrier's AppUserModelID, which its Start Menu shortcut carries too, so harrier pinned from Start and its windows share one button. An elevated harrier's windows aredev.jcollie.harrier.elevated's, on a button of their own. A harrier pinned by a version that did not set its ID yet is a button apart from its windows until it is pinned again. -
Notifications are toasts, one for each pane, replaced and withdrawn as on Linux; a click shows the pane. Windows shows a program's toasts only under an application ID it knows, so the first one registers
dev.jcollie.harrierunderHKEY_CURRENT_USER\Software\Classes\AppUserModelId, with harrier's name and an icon it writes to%LOCALAPPDATA%\harrier. Windows takes them away when harrier exits. -
The desktop's settings are Windows', as the portal's standard appearance keys:
ctx.appearancehas the color scheme programs are set to, the accent color, high contrast, and whether animation effects are off. A change to one evaluates the configuration again, as on Linux.@settingsees only those keys, inorg.freedesktop.appearance. -
Progress goes on every window's taskbar button, where a dock shows it on Linux: the mean of every pane that says how far it is, red while one reports an error or is waiting on the user, full red when none says how far. Each window has a button, and Windows may combine them, so each shows the programs' progress in all of them.
Not there: the search provider, Terminal Intent, cgroups and service mode, which are the Linux desktop's.
Usage #
harrier [options] [-e command [args...]]
harrier validate-config [--hostname NAME] [--color-scheme SCHEME] FILE
-e, --command PROG ARGS... run PROG in the window instead of the shell
--serial PATH open a serial port instead of running a program
--baud RATE the serial port's rate (default 115200)
--config PATH the configuration file to read; also keeps
the window in this process
--no-config read no configuration at all, not even the
default file: the built-in defaults; also
keeps the window in this process
--profile NAME open the profile NAME, as the profile
selector would
--working-directory DIR start the program in DIR
--focus-pane PRIMARY:ID show pane ID of the harrier whose process
ID is PRIMARY, as its jump list does on
Windows; its last window, or a new one,
when that harrier has gone
--tab open a tab in the harrier window last used,
rather than a window
--elevate on Windows, run as administrator: in a
harrier of its own, after Windows asks
--standalone open the window in this process rather than
handing it to a harrier already running
--service run as the session bus service, with no
window until one is asked for
--validation turn on Vulkan's validation layer
--version, -h, --help
The default key bindings:
| Keys | Action |
|---|---|
ctrl+shift+c / ctrl+shift+v |
copy / paste |
shift+insert, middle button |
paste the primary selection |
shift+page_up / shift+page_down |
scroll a page |
ctrl+shift+home / ctrl+shift+end |
scroll to the top / bottom |
ctrl+shift+arrow_up / ctrl+shift+arrow_down |
scroll a line |
ctrl+shift+z / ctrl+shift+x |
scroll to the previous / next shell prompt |
ctrl+=, ctrl+-, ctrl+0 |
font size larger, smaller, as configured |
ctrl+shift+n / ctrl+shift+q |
new window / close window |
ctrl+shift+, |
read the configuration again |
ctrl+shift+t |
new tab |
ctrl+shift+w |
close the focused pane, and the tab with its last |
ctrl+page_down / ctrl+page_up, ctrl+tab / ctrl+shift+tab |
next / previous tab |
ctrl+shift+page_up / ctrl+shift+page_down |
move the tab left / right |
alt+1 … alt+8, alt+9 |
that tab, the last tab |
ctrl+shift+o / ctrl+shift+e |
split: a new pane to the right / below |
ctrl+alt+arrows |
the pane in that direction |
ctrl+super+] / ctrl+super+[ |
the next / previous pane |
ctrl+super+shift+arrows |
move the nearest divider two cells that way |
ctrl+super+shift+= |
even the splits out |
ctrl+shift+enter |
zoom the focused pane to fill the tab, and back |
ctrl+shift+s |
open or close the status window: every terminal's progress and program status |
ctrl+shift+f |
search the focused pane's terminal and scrollback; Enter for the next older match, Shift+Enter for newer, Escape to close |
ctrl+shift+g, ctrl+shift+h |
the next older or newer match of the last search, with the search field closed |
ctrl+shift+p |
open or close the profile selector |
Holding shift takes the mouse back from a program that has asked for it, so that text can still be selected.
Dragging a selection past the top or bottom of its pane scrolls the pane, a row at a time for as long as the pointer stays there, so that the selection can take in more than is on screen. Just past the edge it scrolls slowly, to stop on a line; further out, faster.
The tab bindings are GNOME Terminal's and the split bindings Ghostty's.
alt+1 to alt+9 are taken from programs that use them, as GNOME Terminal
takes them; .unbind them to give them back. close_tab closes a whole
tab, and has no key of its own.
Each pane has a scrollbar over its right edge, shown while the pane is scrolled back or the pointer is at that edge: dragging the thumb scrolls, and a press on the track jumps there. The prompt keys need a shell that marks its prompts (OSC 133): fish does on its own; bash and zsh need shell integration, such as Ghostty's or kitty's.
A press in a pane focuses it; the focused pane is drawn as it is and the others faded toward their backgrounds, with a hollow cursor. Keys go to the focused pane, the wheel to the pane under the pointer. A divider is dragged to move it, and a pane whose program exits gives its place to its neighbour.
Panes are rearranged by dragging them. With the pointer near the top of a pane, in a tab of more than one, a handle shows at the middle of its top edge; taken by it, the pane can be dropped on another, beside whichever of that pane's edges the pointer is nearest, the half it would take marked while it is held there. The two share that pane's place, the one dropped taking the focus, and what the moved pane leaves goes to its old neighbour.
On the tab bar, a press shows a tab and dragging it along the bar moves it,
the middle button or the × closes one, and + or a double-click on the
empty part opens one.
Dragged well above or below the bar, a tab leaves its window, through the compositor's drag and drop, with a picture of it under the pointer:
- dropped on a tab bar — another window's, or its own — it goes there, where a line marks the place. A window with one tab and no bar takes it along its top;
- dropped anywhere else, or the drag cancelled, it gets a window of its own;
- a window whose last tab leaves closes. Closing the last
tab closes the window. With
keep-terminal-openor a program that could not be started, a tab stays after its program has gone, and a key closes it.
Profiles #
A profile is a named way of opening a pane: an ssh to a host, a serial
console, or any other program, listed in the configuration's profiles.
Each can set environment variables for its program and give its panes
colors of their own, so that a window on another machine looks like one.
.{
.profiles = .{
.{
.name = "router",
.command = .{ "ssh", "admin@router" },
// The router has no terminfo entry for harrier.
.env = .{ .{ .name = "TERM", .value = "xterm-256color" } },
.colors = .{ .background = "#2a1010" },
},
.{ .name = "nas", .command = .{ "ssh", "nas.lan" } },
.{
.name = "console",
.serial = .{ .path = "/dev/ttyUSB0", .baud = 9600 },
.open = .tab,
},
},
}
A profile has:
name: what the profile selector lists it as, and its window's title until its program sets one. Each profile needs a name of its own.commandorserial: the program to run, or the serial port to open, with the same settings as the top-levelserial. With neither, the profile runs the shell.cwd: where the program starts.env: environment variables, asnameandvaluepairs, set over harrier's own,TERMincluded.grapheme_width:.unicodeor.legacy, in place of the configuration's (see Configuration).bell_sound: what its panes' bell plays, in place of the configuration'sbell.sound, written the same way (see Configuration).clipboard_readandclipboard_write:.ask,.allowor.deny, in place of the configuration'sclipboard.readandclipboard.write:.denyfor a host whose programs should never see the clipboard, say (see Configuration).colors:foreground,background,cursorandpalette, each in place of the configuration's. Whatever is left out comes from the configuration, and follows it when it changes, light and dark alike. The title bar and tab bar take the colors of the focused pane, so a window on another machine is colored all over.open:.window(the default) or.tab, in the window the profile selector was opened from.elevate: on Windows,trueruns it as administrator, after Windows asks, in a harrier of its own (see Windows).harrier --profile NAMEopens a profile from the command line, the same way.
On Windows, in %APPDATA%\harrier\config.struthio, with each \ in a
path written \\:
.{
.profiles = .{
// PowerShell as administrator, after Windows asks.
.{ .name = "admin pwsh", .command = .{ "pwsh.exe", "-NoLogo" }, .elevate = true, .colors = .{ .background = "#2a1010" } },
.{ .name = "cmd", .command = .{"cmd.exe"}, .open = .tab },
// A WSL distribution, in its home directory.
.{ .name = "ubuntu", .command = .{ "wsl.exe", "-d", "Ubuntu", "--cd", "~" } },
// Git's bash, as a login shell.
.{ .name = "git bash", .command = .{ "C:\\Program Files\\Git\\bin\\bash.exe", "--login", "-i" } },
// Windows' own OpenSSH.
.{ .name = "router", .command = .{ "ssh.exe", "admin@router" }, .env = .{ .{ .name = "TERM", .value = "xterm-256color" } } },
// Visual Studio's developer shell.
.{
.name = "vs dev",
.command = .{ "pwsh.exe", "-NoExit", "-Command", "& { Import-Module \"$env:ProgramFiles\\Microsoft Visual Studio\\2022\\Community\\Common7\\Tools\\Microsoft.VisualStudio.DevShell.dll\"; Enter-VsDevShell -VsInstallPath \"$env:ProgramFiles\\Microsoft Visual Studio\\2022\\Community\" -SkipAutomaticLocation }" },
},
.{ .name = "console", .serial = .{ .path = "COM3", .baud = 115200 }, .open = .tab },
.{ .name = "projects", .cwd = "C:\\Users\\me\\src" },
},
}
harrier --profile "admin pwsh" opens the first from a shortcut or the
Run dialog.
ctrl+shift+p, or the ▾ button, opens the profile selector over the
window. The button is at the left of harrier's title bar and beside the
tab bar's +, and is only there when there are profiles. Typing narrows
the list with zf's fuzzy matching: each word typed must be found,
its letters in order but not necessarily together, in a profile's name or
in what it runs. rtr finds router, and ssh bld the profile named
build-server that runs ssh builder. The best matches come first: those
whose words were all found in the name, then by how closely the letters
sit together and how near the start of a word they fall. The letters found
are underlined.
| Keys | Action |
|---|---|
arrows, ctrl+p / ctrl+n |
the previous / next profile |
page_up / page_down |
a page up / down |
| Enter, or a click | open the profile where it says |
| Shift+Enter, or a shift-click | open it in the other place: a tab instead of a window, or a window instead of a tab |
ctrl+u |
clear what has been typed |
Escape, ctrl+shift+p, or a click outside |
close the profile selector |
Pastes and the middle button go into the profile selector while it is open.
A new tab or split made from a profile's pane runs the profile again, with its environment and colors: a split of an ssh session is another ssh session to the same host. A serial port cannot be opened twice, so a split of a serial console runs the shell, in the console's colors.
When a profile's program exits with an error, its pane stays open to say how it ended, so an ssh that could not reach its host says why rather than its window vanishing. A key closes it. A program that cannot be started at all, or a port that cannot be opened, is reported the same way.
TERM and terminfo #
harrier has a terminfo entry of its own, harrier (also named
xterm-harrier). It describes everything harrier's terminal does that
xterm-256color does not:
- 24-bit color;
- curly and colored underlines, and overlines;
- cursor shapes, and resetting the cursor to the configured one;
- synchronized output and bracketed paste;
- focus reports and SGR mouse reports;
- OSC 52 clipboard reads and writes;
- clearing the scrollback.
harrier's terminal is Ghostty's, so the entry is Ghostty's, under harrier's
names. Its source is dist/terminfo/harrier.terminfo.
zig build installs the source, the compiled entry under share/terminfo
(when ncurses' tic is on the path) and a termcap version,
share/terminfo/harrier.termcap. harrier points the programs it runs at
the copy beside it with TERMINFO, so on the machine harrier runs on,
TERM=harrier, the default, needs nothing else.
A machine reached over ssh needs the entry too, or programs there stop with "unknown terminal type". To install the entry on a server:
-
With Nix, the
harrier-terminfopackage holds the entry alone. It builds in seconds and pulls in nothing of harrier's:nix profile install git+https://git.jcollie.dev/jeff/harrier.git#harrier-terminfo -
On NixOS, the module installs it alone with
programs.harrier.terminfo.enable = true;(programs.harrier.enableinstalls it with the rest of harrier). -
Anywhere else, copy it from a harrier terminal:
ticcompiles it into~/.terminfoon the server, or into the system database when run as root.infocmp -x harrier | ssh server -- tic -x -
Where the entry cannot be installed, xterm-256color is on every machine
already, at the cost of what it does not describe:
.{ .term = "xterm-256color" }
or, for one ssh session, on its command line, since ssh hands the server
the TERM it was started with:
TERM=xterm-256color ssh old-server
or, for a host visited often, in a
profile for it, with TERM in its env.
Translations #
harrier's own text is in locales/, one Fluent file per
language, en-US.ftl the source the others follow. That covers:
- the status window;
- the search field: its placeholder and its count of matches;
- program status notifications;
- what a pane says when its program has gone or could not start;
- the names of actions, as the desktop shows them when it asks about global shortcuts.
Log messages and the command line's help stay in English.
The language is chosen when harrier starts, from LANGUAGE, LC_ALL,
LC_MESSAGES and LANG, or from the configuration's language, a list
in the same form as LANGUAGE:
.{ .language = "pt-BR:pt" }
The closest translation wins: the same tag, then the same language and
script, then the same language; with none, English. A message a translation
leaves out comes from English. Numbers in messages are written the way
LC_NUMERIC says. Changing language in a running harrier takes effect
when the configuration is reloaded.
Translations are embedded in the binary, so there is no data directory to install. CONTRIBUTING.md says how to add one.
Running as a service #
zig build -Dbindir=/where/harrier/is installs five things beside the
binary. Each of the first three names the binary by its absolute path, which
is why the option exists; the Nix package passes it.
share/applications/dev.jcollie.harrier.desktop: the desktop entry, withDBusActivatable=true,Implements=org.freedesktop.Terminal1and a New Window action.share/dbus-1/services/dev.jcollie.harrier.service: lets the session bus start harrier when a window is asked for, through systemd.share/systemd/user/app-dev.jcollie.harrier.service: harrier as a user service,harrier --service. It starts with no window, opens the ones it is asked for, and exits five minutes after the last one closes (seeservice.idle_exit_seconds). Stopping it closes its windows.share/icons/hicolor/*/apps/dev.jcollie.harrier.png: the icon.share/gnome-shell/search-providers/dev.jcollie.harrier.search-provider.ini: GNOME Shell's search, below. This one is installed without-Dbindir.
GNOME Shell's search #
On Windows, the jump list is what finds a pane; see Windows.
/dev/jcollie/harrier/SearchProvider serves
org.gnome.Shell.SearchProvider2. As the overview is
typed in, it finds the open panes by their title and location, and shows
each with its title, its location, and harrier's icon. It matches as the
profile selector does, with zf: each word typed must be found, its
letters in order, ignoring case, in the title or else the location, so
hrr finds ~/dev/harrier. The location is as the title bar's subtitle
writes it — ~/dev/harrier, or router:/etc/nginx for a shell on another
machine — so a host's name finds the panes on it, and it is matched as a
path, a match in its last part counting for more. The best matches come
first, those found in the title before those found only in the location.
Picking one shows that pane, in its tab, and raises its window.
After the panes come the profiles, best first, found by their name or else
what they run, each shown as its name and "Profile · " and what it runs:
router finds both the ssh session already open to the router and, after
it, the profile that would open another. Picking a profile starts it as
the profile selector would, in a window or a tab of the last window as it
says. Picking harrier's own icon beside the results shows the first pane
found, or else starts the first profile found, or opens a window when
neither is.
- Only while harrier runs. The registration file says
AutoStart=false, so the Shell asks only a harrier already on the bus. Without one there are no panes to find, and though there are profiles, starting harrier to offer them would cost a font scan on the first keystroke of every search in the overview, for anyone with harrier installed. - Raising the window. The Shell passes only a timestamp, not an
activation token, and the token harrier could ask for itself carries the
serial of its own last input, which is stale — the click went to the
Shell — so Mutter would only say "harrier is ready". On GNOME, harrier
registers a startup sequence with
gtk_shell1.notify_launch, Mutter's protocol with GTK, and activates the window by that, as GTK does for what it launches; focus stealing prevention still applies. Elsewhere it asks for a token as usual. - Where it is found. The Shell reads registration files from
$XDG_DATA_DIRS, not~/.local/share. The NixOS module links them into the system profile; with Home Manager alone,useUserPackagesputs the package in a profile that is on that path. - Where its results appear is the user's to choose: GNOME Settings' Search page orders the providers and switches them off.
It can be tried without the Shell:
$ busctl --user call dev.jcollie.harrier /dev/jcollie/harrier/SearchProvider \
org.gnome.Shell.SearchProvider2 GetInitialResultSet as 1 vim
as 1 "pane-3"
$ busctl --user call dev.jcollie.harrier /dev/jcollie/harrier/SearchProvider \
org.gnome.Shell.SearchProvider2 ActivateResult sasu pane-3 1 vim 0
The Terminal Intent #
The same object, /dev/jcollie/harrier, serves org.freedesktop.Terminal1
beside org.freedesktop.Application, as the Terminal
Intent asks. LaunchCommand opens one window with a tab
for each command it is given, as the specification suggests, or with the
shell for none:
execis the program and its arguments; empty runs the shell.envis set on top of harrier's environment rather than replacing it.working_directoryis where it starts. Without one, the desktop entry'sPathis used, and failing that$HOME.desktop_entry, when given, names the tabs after the entry'sName. One that is not a desktop entry refuses the call withorg.freedesktop.DBus.Error.InvalidArgs.keep-terminal-openkeeps each tab after its program exits, saying how it exited, until a key is pressed.- The activation token in
platform_datagoes to the window.
A program that cannot be started still gets its tab, saying why and
waiting for a key, and the rest of the commands are launched regardless.
The paths and arguments are ay; the NUL that GLib's bytestrings end with
is dropped.
To try it by hand, busctl takes each ay as a length and its bytes:
$ busctl --user call dev.jcollie.harrier /dev/jcollie/harrier \
org.freedesktop.Terminal1 LaunchCommand 'aa{sv}aya{sv}a{sv}' \
1 1 exec aay 1 3 116 111 112 \
0 1 keep-terminal-open b true 0
(gdbus call with b'…' literals nested in variants is no use here:
it sent corrupted bytes when this was tested.)
On NixOS, the flake's module does all of it:
{
inputs.harrier.url = "git+https://git.jcollie.dev/jeff/harrier.git";
# in the NixOS configuration:
imports = [ inputs.harrier.nixosModules.harrier ];
programs.harrier.enable = true;
}
It installs the package, with its terminfo entry; the D-Bus service file and the systemd user unit
(services.dbus.packages, systemd.packages; programs.harrier.service.enable
turns them off); and links share/gnome-shell/search-providers into the
system profile (programs.harrier.searchProvider.enable), which NixOS does
not do by default — only GNOME's own module links all of share. On a
server only reached from harrier over ssh, programs.harrier.terminfo.enable
installs the terminfo entry alone. The
configuration file is each user's, and the Home Manager
module writes and checks it.
The service gets its environment from the systemd user manager, which needs
the graphical session's: WAYLAND_DISPLAY above all. Desktop sessions
export it at login with dbus-update-activation-environment --systemd WAYLAND_DISPLAY, or systemctl --user import-environment. A window handed
over from a shell still runs with that shell's environment and directory,
because the launch sends both.
Configuration #
harrier reads harrier/config.struthio from the configuration directory
known-folders finds: $XDG_CONFIG_HOME, or ~/.config.
Without the file, it uses the defaults, and --no-config uses them
whatever files there are — to see whether a problem is the
configuration's. A struthio file is ZON that can make
decisions:
.{
.font = .{
.family = "Iosevka Term",
.size = if (ctx.hostname == "laptop") 11 else 13,
.features = .{ "ss05" },
},
.colors = .{ .background = "#1d1f21", .foreground = "#c5c8c6" },
.cursor = .{ .style = .bar, .blink = false },
.command = .{ "fish", "--login" },
.keybind = .{
.{ .keys = "super+c", .action = .copy },
.{ .keys = "ctrl+shift+n", .action = .unbind },
},
}
ctx.hostname and ctx.os say where it is running, and @env("NAME")
reads the environment.
link_detection, on by default, makes URLs in the text open with
ctrl+click as hyperlinks a program makes (OSC 8) do; off, only those do.
grapheme_width is how many cells a grapheme takes. .unicode, the
default, gives it the width Unicode does, so that ➡️ and 👍🏽 are wide, and
turns on mode 2027 for programs to see. .legacy gives it the width of
its first code point, which is what a program counting with an older
wcwidth expects. Either way a program can switch to the other with mode
2027, and a change applies to panes opened afterwards. A profile can set
its own, for a host whose programs count the old way:
.{ .profiles = .{ .{ .name = "old box", .command = .{ "ssh", "old-box" }, .grapheme_width = .legacy } } }
cgroup puts each program in a systemd scope of its own, on by default:
.{
.cgroup = .{
.memory_high = 4 * 1024 * 1024 * 1024, // MemoryHigh, in bytes
.tasks_max = 4096, // TasksMax
.required = false, // true: no scope, no program
},
}
Every scope is also given ManagedOOMMemoryPressure=kill, so that
systemd-oomd kills that program rather than harrier. Without systemd, a
program runs in harrier's own cgroup, with a warning, unless required is
set. .enabled = false turns scopes off.
notifications says when a program's desktop notification is shown,
asked for with OSC 9 or OSC 777 or sent for its program status:
.unfocused, the default, only when its pane is not the one being
looked at; .always; or .never.
bell says what a program ringing its pane's bell does, each on its own:
sound: what it plays..none, the default, plays nothing..desktopplays the desktop's own bell sound, through xdg-system-bell-v1, or GNOME's own protocol, or Windows'; nothing where the compositor has neither, as on sway..{ .file = .{ .path = "...", .volume = 0.5 } }plays a sound file — WAV, FLAC, Opus or MP3, of at most 30 seconds — through PipeWire or Windows' audio, at a volume from 0 to 1 (1 if left out), under the desktop's own volume for notifications.~/in the path is the home directory, and a relative path is taken from the directory of the configuration file, even when it is written in a file that one@includes. The file is read when the configuration is loaded, and again on every reload; one that cannot be played — not there, not a format harrier reads, too long, or no sound output — rings the desktop's bell instead, and the log says why. A bell rung while its sound is still playing lets it finish. A profile'sbell_soundtakes the place ofsoundfor its panes.flash: the pane lightens for a moment. Off by default.attention: when the window does not have the focus, the desktop is asked to show that it wants attention, until it is focused. On by default.mark: 🔔 before the title of the window, and of the tab, holding a pane whose bell rang while it was not being looked at, until it is. On by default.notify: a desktop notification, asnotificationsallows. Off by default.
Attention, the mark and the notification come only for a pane that is not being looked at — the focused pane of the shown tab of the focused window — and once until it is; rings a quarter of a second apart or closer count as one.
.{ .bell = .{ .sound = .{ .file = .{ .path = "~/sounds/bell.flac" } }, .flash = true } }
clipboard says what programs may do with the clipboard, and which
pastes harrier asks about first. Asking is a confirmation over the pane:
Enter gives the highlighted answer, which starts on the one that does
nothing, Escape is that one, and Left, Right and Tab move between them.
The text that would be pasted or written is shown as it is, in a box of
the pane's own colors under a line saying how many lines and bytes it
is: each line on a row of its own, its line break marked ⏎ — \n,
\r\n, or a lone \r, any of which reaches the program as Enter —
a long line wrapped with ↪ before each row after its first, tabs at
stops of eight, and control characters shown dim as their pictures (␛,
␇, ␡), or as � with what is not UTF-8. The confirmation grows with the
text until it fills the pane, and then the text scrolls: with the wheel,
Up and Down, and PageUp and PageDown.
read: a program reading the clipboard or the primary selection, with OSC 52 or with kitty's clipboard protocol (OSC 5522). Anything run in a pane, an ssh's far end included, could take what was copied, a password say, so.ask, the default, asks first: Allow, Allow for this pane, which holds until the pane closes, or Deny..allowlets every program read without asking, and.denyrefuses them all without asking. A program refused with OSC 52 is answered with an empty clipboard, as xterm answers it; with kitty's protocol, withEPERM. With kitty's protocol a program may read any of the clipboard's types, an image say, and is handed text under whichever text type it asks for; OSC 52 reads only text. On Windows a bitmap on the clipboard, which is what most screenshot tools put there, is read asimage/png. Two of kitty's reads are not asked about, as kitty does not ask: one for only the list of types on the clipboard, which.denystill refuses; and a program fetching what the user has just pasted, which is never refused. A program that turns on kitty's paste events (mode 5522) is told of a paste rather than sent it, and fetches it that way.write: a program putting something on the clipboard, with OSC 52 or kitty's protocol, in place of what is there: text, or with kitty's protocol any types, an image beside its text say..allow, the default, lets it;.askasks first, showing the text, a PNG image itself above it, and each type with its size;.denyrefuses it. A program writing with kitty's protocol is told how it went once it has:DONEwhen the write is on the clipboard,EPERMwhen it was refused. On Windows an image is offered as a bitmap as well as a PNG, for the programs that take nothing else, and a type Windows has no format for is put there under its own name.confirm_unsafe_paste: asks before a paste that would run as it arrives, showing the text: one with a line break in it, into a program that has not asked for bracketed paste. Paste or Cancel. Every way of pasting to a program is asked about: the paste key, the selection's, the middle button, and dropping text or files. On by default. A paste into a program that turned on kitty's paste events is not asked about: nothing of it runs as it arrives, since the program fetches it itself.toast: a toast, a line at the bottom of the pane for a moment, saying what was just done with the clipboard or the primary selection without asking: what the user copied with the copy key ("Copied 42 characters"), and what a program wrote or read, named by its foreground program ("ssh copied 3 lines to the clipboard", "nvim read the primary selection"). Text is counted in lines, or in characters when it is one line; anything else is listed by type and size. A program writing nothing has "Cleared the clipboard"..always, the default, shows them all;.programsonly what programs do;.nevernone. Copying by selecting has none, since it happens at every release of the button, and neither has anything the user has just answered a confirmation about. A copy that fails says so whatever this is. A newer toast replaces the one shown; how long one stays iswindow.toast_ms.
A pane asking while it is not being looked at waits for an answer there, and 📋 marks its tab and the window's title until it is looked at; the window asks for attention as for a bell. A pane asks one thing at a time: a program asking again replaces its own question, which is answered no, and a paste waiting to be confirmed is never replaced.
.{ .clipboard = .{ .read = .deny, .write = .ask } }
A key binding with .global = true works wherever the focus is, not
only in harrier's windows: a global hotkey.
.{
.keybind = .{
.{ .keys = "super+backquote", .action = .present_window, .global = true },
.{ .keys = "ctrl+alt+t", .action = .new_window, .global = true },
},
}
present_window raises the window last used, or opens one when there is
none; new_window and new_tab open one; any other action acts on the
window last used, raised. harrier binds them, in this order of choice:
- through the compositor, with xx-hotkey-v1, where it has it;
- through the compositor, with vicinae-hotkey-v1, the same protocol under the name some compositors shipped first — Hyprland, from 0.56;
- through the desktop portal's GlobalShortcuts, which KDE Plasma and GNOME, among others, provide.
With either protocol, the binding is exactly the key asked for, for as long as harrier runs, and a key the compositor refuses or takes back is said in the log with its reason. The portal takes the keys only as what harrier would prefer: the desktop may ask the user, and the user may choose others, which it remembers by action. Only the harrier that owns the session bus name binds them, so a second one does not take them from the first. A hotkey's press is an input the compositor counts, so the window it raises or opens takes the focus.
drag_and_drop, on by default, lets programs take part in drag and drop
with kitty's protocol. Off, harrier ignores the protocol, as a terminal
that has never heard of it would, and programs fall back to whatever they
do without it.
images says what programs may do with kitty's graphics protocol:
.{
.images = .{
// Whether programs may show images at all; off, a program
// asking is told they are not supported.
.enabled = true,
// The most of its images' pixels each terminal keeps, per
// screen, before the oldest go.
.storage_bytes = 320 * 1000 * 1000,
// Whether a program may send an image as a file, a temporary
// file or shared memory. Never for a serial port, whose other
// end is another machine, and whose file names would be read
// here.
.files = true,
},
}
Including other files #
@include("path") stands for another file's value, so a configuration can
keep a theme, or its key bindings, in a file of its own:
let theme = @include("themes/one.struthio");
.{ .colors = if (ctx.appearance.color_scheme == .light) theme.light else theme.dark }
A relative path is resolved against where the including file really is,
symbolic links followed: a config.struthio linked from a dotfiles
repository includes the files next to it there. Included files are read
with the configuration, and again on every reload. A mistake in one is
reported at that file and line.
Following the desktop #
harrier reads the desktop's settings from the XDG desktop
portal (org.freedesktop.portal.Settings) when it
starts, watches them, and evaluates the configuration again whenever one
changes, applying the result to everything open; on Windows, from
Windows' own (see Windows). The configuration sees them two
ways:
ctx.appearance, the portal's standard appearance keys:color_scheme(.dark,.lightor.no_preference),accent_color("#rrggbb", or null when the desktop has none),contrast(.normalor.high) andreduced_motion(.reduceor.no_preference).@setting("namespace", "key"), any setting the desktop publishes, or null: a string, a number, a boolean, or a list.
let light = ctx.appearance.color_scheme == .light;
.{
.colors = .{
.background = if (light) "#fafafa" else "#1d1f21",
.foreground = if (light) "#383a42" else "#c5c8c6",
.cursor = ctx.appearance.accent_color,
},
.font = .{ .family = @setting("org.gnome.desktop.interface", "monospace-font-name") orelse "monospace" },
}
A reload changes the colors — the defaults, so a color a program set
stays — the font, the cursor, the key bindings, the padding and the tab
bar of every window open. What is fixed when a window is made, the program
it runs and who draws its frame, applies to new ones. A configuration that
no longer evaluates is reported and the one in use kept. ctrl+shift+,
reloads by hand, after editing the file. Without a portal, the
configuration sees the defaults; validate-config checks a file under
every color scheme.
Every field is in src/config/Config.zig:
-
font: the family, as a fontconfig pattern; the size in points; OpenType features; ligatures on or off; andbuiltin_glyphs, on by default, which draws box drawing, blocks, braille and the like to fit the cell rather than taking them from the font. -
colors: the foreground, the background, the cursor, and palette overrides. A color is anything libghostty-vt parses, X11 names included.progresscolors the progress bars:.normal,.@"error",.paused(for waiting on the user too) and.done, each unset by default, for the palette's blue, red, yellow and green as the program has them.searchcolors search matches:.backgroundand.foregroundfor each,.selected_backgroundand.selected_foregroundfor the one selected; Ghostty's yellow and orange by default. -
cursor: the style and whether it blinks. -
window: the padding, the initial size in cells, and intitle_bar, who draws the title bar and frame:.server(the default) asks the compositor, and harrier draws its own where the compositor will not, as on GNOME or weston;.clientalways draws harrier's;.nonedraws nothing.
harrier's own title is drawn in the desktop's interface font, as the desktop's own title bars are: GNOME's
font-name, through the portal's settings —Cantarell 11, say — or sans-serif at 10.5 points.And
subtitle, a second, smaller line under harrier's own title:.location(the default) shows where the focused pane's program is, and.nonenothing. A location on this machine is its directory, with the home directory written~; on another it ishost:path, as scp writes it. Only the shell can say where it is on another machine, with OSC 7, which its prompt can be set up to send, so nothing is guessed from an ssh command line. On Linux, until a shell first says, the location is the directory the program on the pty is in. A location too long to fit loses the start of its path. The bar is two lines tall whenever the subtitle is.location, whether or not there is a location to show, so the terminal keeps its rows as one comes and goes. A title bar the compositor or Windows draws has no subtitle.And
tab_bar:.auto(the default) shows the tabs once there are two,.alwaysfrom the first,.nevernot at all. When the bar comes or goes, a floating window grows or shrinks by its height, so the terminals keep their size; a maximized, fullscreen or tiled window keeps the size the compositor gave it, and the terminals give up the room or take it back. Andscrollbar:.auto(the default) while a pane is scrolled back or pointed at,.alwayswhenever it has scrollback,.nevernot at all. Andresize_overlay: when a pane's columns and rows change, they are shown in a box at its middle forresize_overlay_ms(750 by default) —.after_first(the default) once the pane has been shown at its first size,.alwaysfrom that first size on,.nevernot at all.toast_ms(3000 by default) is how long a toast, such as the clipboard's, stays over its pane; 0 shows none.progress_bar = falsedraws no progress bars, on panes or tabs, anddock_progress = falseputs none on harrier's icon, or on Windows' taskbar buttons.And
opacity, from 0 to 1 (1 by default): how opaque the background is. Text, and cells a program colored itself, stay opaque, as do harrier's title bar and tabs.blur = trueasks the compositor to blur what is behind a translucent window, through ext-background-effect-v1; where the compositor does not offer it, nothing is blurred and the log says so. -
commandorserial: what the window shows.serialtakes the path, rate, data bits, parity, stop bits and flow control. -
term: whatTERMis set to (harrier, harrier's own terminfo entry). SeeTERMand terminfo. -
language: the language harrier's own text is in, as a list of tags ("de","pt-BR:pt"). Unset follows the environment. See Translations. -
scrollback_bytes: how much scrollback to keep. -
present:.auto,.dmabufor.shm. -
gpu: part of a Vulkan device's name ("Radeon","llvmpipe"), to choose one on a machine with several. Unset uses the device the compositor composites with. -
service:idle_exit_seconds, how long the service keeps running without a window. The default is 300; null keeps it running. -
keybind: bindings added on top of the defaults, and global hotkeys, with.global = true. -
profiles: named ways of opening a pane, which the profile selector lists. See Profiles. -
foreground_poll_ms: how often, in milliseconds, each pane's foreground program is looked for (250 by default), for a configuration that readsctx.foreground; 0 looks only when a key is pressed and when the program writes something. See Following the foreground program.
Following the foreground program #
On Linux, a configuration can depend on what each pane's terminal
belongs to right now — its foreground program: the editor or ssh or
root shell the pane's shell has handed the terminal to, or the shell itself
when nothing else is running. ctx.foreground is null, or:
name: what it was started as, without the directory (ssh,nvim);exe: where its executable is, links followed, or null when that cannot be read, as for another user's program;uid: the user it runs as, its effective UID;user: that user's name in/etc/passwd, or null. A user known only to systemd-homed or a directory service has none.
.{
.colors = .{
.background = if (ctx.foreground) |f| switch (f.name) {
"ssh" => "#002030",
"pwsh" => "#012456",
else => if (f.uid == 0) "#3a0000" else "#1d1f21",
} else "#1d1f21",
},
.keybind = if (ctx.foreground) |f| (if (f.name == "nvim") .{
.{ .keys = "ctrl+shift+c", .action = .unbind },
} else .{}) else .{},
}
A configuration that reads ctx.foreground is evaluated again for each
pane, with its foreground program, and the pane takes from that its
colors, its keybind entries but the global ones, bell, cursor,
clipboard, link_detection, notifications, drag_and_drop and
images, with its profile's colors, bell sound and clipboard access over
them. Everything else — fonts, windows, the profiles, global bindings —
comes from the evaluation that sees ctx.foreground as null, as do
grapheme_width, fixed for a pane's life, and all of a serial port's or a
message's. A pane's evaluation giving one of those another value is
logged, and reported by validate-config, and has no effect. Each
program is evaluated once and kept until the next reload; one whose
evaluation fails is logged, and its panes keep the app's configuration. A
configuration that never reads ctx.foreground is evaluated once, as
before, and nothing is looked for.
harrier looks before handling each key, so that a program's bindings are
its own from the first key; after the program writes something; and every
foreground_poll_ms, which also notices an exec or a setuid that
leaves the process group as it was. A program that starts and writes
nothing is seen that long after.
What harrier can see is only its own pty's foreground program. sudo runs
its command on a pty of its own, so sudo -i and sudo ssh host are both
sudo, with UID 0; tmux and screen are themselves; and what runs on the
far side of an ssh is invisible. On Windows, whose pseudoconsole has no
foreground process group, ctx.foreground is always null.
Checking a configuration #
$ harrier validate-config ~/.config/harrier/config.struthio
/home/me/.config/harrier/config.struthio: colors.palette[3]: "gren" is not a color
/home/me/.config/harrier/config.struthio: keybind[0]: "hyper+x" is not a key harrier can read
validate-config evaluates the file and checks what struthio sees only as
strings: the colors, the key bindings and the font features. It reports
every problem rather than the first, exits 1 if there were any, and opens no
window. A running harrier would refuse to start over a bad color but only
warn about a bad binding or feature; this treats them all as mistakes.
--hostname NAME sets what the file sees as ctx.hostname, to check a
file meant for another machine. The file is evaluated and checked once for
each color scheme it could see as ctx.appearance.color_scheme —
no_preference, dark and light — so that a mistake on one side of an
if is found whichever side the desktop is on; a problem found under only
some of them says which. --color-scheme SCHEME checks under that one
alone. @setting gives null, as it does without a desktop.
A file that reads ctx.foreground is also checked as a pane would see
it, with each foreground program --foreground NAME[:UID] names — the UID
is yours unless given, and the program has no user name — or, without one,
as sh run by you and by root. A mistake there, f.user.? for a program
with no user name, say, is reported with the program it was found for, as
is anything such a pane says that only the app's evaluation decides.
Home Manager #
The flake has a Home Manager module, homeModules.harrier. It installs
harrier, writes its configuration, and installs the D-Bus service file and
systemd user unit through dbus.packages and systemd.user.packages, so
that one harrier opens every window.
The configuration is a struthio file, given as text or as a file, not generated from Nix settings:
{
inputs.harrier.url = "git+https://git.jcollie.dev/jeff/harrier.git";
# in a Home Manager configuration:
imports = [ inputs.harrier.homeModules.harrier ];
programs.harrier = {
enable = true;
configText = ''
.{
.font = .{ .family = "Iosevka Term", .size = if (ctx.hostname == "laptop") 11 else 13 },
}
'';
# or: configFile = ./harrier.struthio;
# what ctx.hostname is while it is checked; the sandbox is `localhost`
validateHostname = "laptop";
};
}
-
configTextorconfigFile: one or the other, or neither for the defaults. A Nix path inconfigFileis copied into the store; a string naming a file outside it ("/home/me/dotfiles/harrier.struthio") is linked to where it is, so editing it needs no rebuild. -
extraFiles: files beside the configuration, for it to@include, each by its path relative to the configuration, withtextor asource:extraFiles."themes/one.struthio".source = ./themes/one.struthio;They are checked with the configuration, laid out together as they will be installed, so that an include finds the same file at build time.
-
validate(on by default): the file is run throughharrier validate-configas it is built, so a mistake failshome-manager switchrather than the next window. A file linked from outside the store cannot be read by the build and is not checked. The check runs in the build sandbox, where@envfinds almost nothing set. -
validateHostname: whatctx.hostnameis during the check. -
service.enable(on by default): the D-Bus and systemd user service. The service reads its configuration when it starts, and stopping it closes every window, so switching does not restart it; a new configuration applies once it next starts. -
package: harrier from this flake, by default.
Design #
┌────────────── main thread ──────────────┐
compositor ◀──────▶│ aerie ─▶ Window ─▶ frame.Builder │
(Wayland) │ platform │ ─▶ aerie Renderer ─▶ Target ─▶ dma-buf / shm
│ │ lock │
└────────────────┼─────────────────────────┘
▼
termio.Session (Terminal + stream, behind a mutex)
▲ nextSlice │ outbox
IO thread ──┘ ▼
Backend: Pty | Serial | Message
backend/: what is on the other side.Backendis a tagged union of three:Pty, which forks the program onto a pseudo-terminal using raw system calls;Serial, a serial port; andMessage, nothing on the other side but text saying why a program could not be started or a port opened, so that the pane can stay open to say so.- On Windows
PtyisConPty: the program on a pseudoconsole, in a job that ends it with its pane. An anonymous pipe cannot be waited on, so a thread of its own reads what the pseudoconsole writes, a little ahead of the IO thread, and sets an event; the process is waited on as well, since the pipe ends only once the pseudoconsole is closed. The pseudoconsole is theconpty.dllbeside harrier.exe's, loaded by its full path, or Windows' own when it or itsOpenConsole.exeis missing.
desktop: which of the next three there are. On Windows,Instancestands in forActivation,windows/PortalforPortal, andScopesis a stand-in that is never there.Instance: one harrier with many windows on Windows, over a named pipe open only to the user, with a thread of its own that queues the windows asked for asActivation's does. A launch checks the user at the other end before it sends anything. An elevated harrier has a pipe of its own, with a high integrity label, and each end checks that the other is elevated;windows/elevationtells whether a process is, and starts harrier again with the shell'srunas.Activation: the session bus. It owns the name and servesorg.freedesktop.Applicationandorg.freedesktop.Terminal1, on a thread of its own, through zig-dbus-service. Its handlers only queue the windows and tabs asked for and wake the main loop. It is also the client a launch uses to hand its window, or with--tabits tab, over: thenew-windowandnew-tabactions. A click on a notification can come back through it too, asfocus-pane.Scopes: a cgroup for each program. The program's process is forked and waits, before it execs, on a pipe; meanwhile harrier asks the user's systemd for a transient scope holding it (StartTransientUnit, on the session bus), waits until/proc/<pid>/cgroupsays it is there, and then lets it go. So the program, and everything it forks, starts in the scope, rather than being moved there while it runs.Portal: the XDG desktop portal, on a thread and connections of its own: the desktop's settings, watched; and notifications, which the main thread queues for it. Notifications go on a second connection, opened for the first one, so that the harrier owning the bus name can first register asdev.jcollie.harrier(org.freedesktop.host.portal.Registry), which has to come before anything else on a connection. The desktop then shows them as harrier's and activates the application when one is clicked. Any other harrier stays anonymous and names its action withoutapp., so that the click comes back to it asActionInvokedrather than going to the other by name. A notification is named for its process and pane, so that each pane has one at a time and a click finds only its own. Links open on the same connection, so that the desktop opens them on harrier's behalf:OpenURIfor a URI, andOpenFile, with the file open, for a file on this machine, whichOpenURIrefuses.windows/Portal:Portal's part on Windows: the desktop's settings, and notifications as WinRT toasts. The settings are read where Windows keeps them — the registry,UISettings,SystemParametersInfoW— and again when a hidden top-level window of its own is told one changed, which a message-only window is not; on the main thread, so with no lock. zigwin32 has no WinRT, so the few interfaces used are declared there, as far as the slots called; the IID of the generic click handler is derived at compile time, as WinRT derives it, from its signature. A click comes on a thread of Windows' own, is queued under a lock, and wakes the main loop.windows/JumpList: the jump list, replaced whole in oneICustomDestinationListBeginList…CommitList: a category ofIShellLinkWshortcuts for the panes, titled through their property store, and the profiles as tasks. What the items say and run isjump_list, apart from it, so that it is tested on Linux too.links: where a URL in a line of text begins and ends, around the cell clicked. A scheme a desktop opens starts it; the first space, control character or character a URL cannot hold ends it; and the punctuation of the sentence around it, or a closing bracket it did not open, is left off its end. The window gives it the line, soft wraps and all, and a map from its bytes back to the cells.termio/Session: one terminal.- Its IO thread is the only reader and writer of the backend. It feeds what it reads through libghostty-vt's stream with the terminal's lock held.
- Keystrokes, and the terminal's own replies to queries, go out through a locked outbox, so the main thread never blocks on a slow port.
- A resize is made on the IO thread too. Reflowing a long scrollback to a new width takes tens of milliseconds, so the main thread only says what size is wanted, and the IO thread makes the latest when it gets to it: a divider dragged across forty columns reflows a few times, not forty. Meanwhile the window draws the pane as it last was, cut to its new size, rather than wait for the terminal's lock. Nothing else in the main loop waits for it either: a title or progress report that arrives mid-reflow is taken on a later turn, and the panes published to GNOME Shell's search keep a busy pane's last known directory. A reflow of a large scrollback takes hundreds of milliseconds, and a wait for it would stop the whole window — a divider dragged across would follow in jumps.
- zig-aerie: the window system, the renderer and the fonts, in a
library of their own. harrier uses them as any program would.
platformis the window system, chosen at compile time — Wayland, or Win32 on Windows — speaking the portable types of itsEvent.zig. harrier names its window menu's items when it starts it, and a tab dragged out is the platform's local drag, offered under a type made from harrier's application ID that only its own windows take. On Windows,waitisMsgWaitForMultipleObjectsExon the thread's queue and the handles the app names. Such a wait takes at most 64 handles, so every session signals one event the app shares between them, rather than one of its own; woken, the windows ask each pane for its events, as they do every turn anyway. While a window's frame is dragged, Windows runs a loop of its own inside the dispatch, so the platform runs a turn of harrier's from there (setModalTurn) — on each new size and on a timer — and the window is drawn at the size it is being dragged to.gpudraws a frame of instanced quads in one call, plus one for each image, which is a texture of its own, sampled with filtering as it is scaled; textures not drawn for a while, or past a budget, are let go. ItsTargetsays where the frame goes: dma-bufs or shared memory on Wayland, a swapchain onVK_KHR_win32_surfaceon Windows.textis the fonts, scanned once (Fonts), a family in four styled fallback lists (Family), and the glyph cache and its atlases (Glyphs,Atlas).decoris the title bar and frame;desktopthe portal's settings.
renderer/:frame.Builderturns libghostty-vt'sRenderStateinto zig-aerie's rectangles.imagesreads where a terminal's kitty images are, with it locked, into pixel positions, and copies out only the pixels of those the renderer does not hold yet: an image is uploaded once, and again only when it is sent again or its animation moves on. An image is named to the renderer by its pane, and its screen and number there.
Window,TabandPane: a window is the platform window, the renderer and its tabs; a tab is a tree of splits with panes at its leaves, and which pane has the focus; a pane is a session and what the window keeps about it — what was last drawn of it and its rows shaped, its selection, its title, where it is laid out. The tree is only geometry: where each pane goes, which one a point or a direction finds, which divider the pointer is on. Only the shown tab's panes are drawn, but every pane's session is polled, so a hidden program's output is read and its exit noticed, and every pane is resized with the window.decor/: the decorations — everything harrier draws in a window besides the terminal.decor/decorhas what they all draw with: colors mixed from a pane's, and titles cut to fit.decor/TitleBar: harrier's own title bar and frame, which is zig-aerie's: the insets around the grid, the hit-test that turns a press into a move or resize request, and the title and subtitle, drawn into the same frame as the grid. harrier keeps room in it for its two buttons, the profile selector's at the left and the status button left of the window's own, and draws those. The title is in the desktop's interface font, a familyApp.titleFamilymakes the first time a window draws its own bar; the subtitle is that family at 0.8 of its size, and a location too long for it is cut at its start, keeping its host. Where the window system draws the frame around it, as Windows does, it is the bar alone.location: a pane's location, from the URL in an OSC 7 report or a directory/procnames, and how it is written.StatusWindow: the status window, a toplevel of its own with a renderer of its own, made a child of a terminal window withxdg_toplevel.set_parent, and handed to another when that one closes. It also works out whatdecor/StatusButtonshows: the count of reports and the most pressing of them, which the title bar, the tab bar or the badge carries.Search: a pane's search: what has been typed, and libghostty-vt's searcher for it. The window feeds it with the terminal locked, a screen and a stretch of scrollback at a time, and it works through that with the lock let go, a turn of the loop at a time; matches in view become highlights in the render state, which the frame colors.l10n: harrier's text through zig-fluent. It holds one bundle per translation inlocales/, embedded by a modulebuild.ziggenerates, chooses one at startup, and falls back to English for a missing message. Text with no arguments is formatted once and kept, since some of it is drawn every frame.decor/TabBar: the strip of tabs, drawn by harrier under whoever's title bar, likewise as insets, a hit-test and quads, and the arithmetic of dragging along it and dropping onto it. A tab dragged off it goes through the platform's drag and drop (wl_data_device), offered as a type only harrier's own windows take; its picture is drawn by zig-aerie'sgpu.cpu, which draws the renderer's instances on the CPU into shared memory. On Windows every window a tab can go to is this process's, so the drag needs no OLE: the window it came from keeps the mouse captured, each move finds the window under the pointer, and the release drops it there; its picture follows the pointer in a layered window the pointer goes through.Dnd: kitty's drag and drop protocol, connected to the window system. libghostty-vt keeps the protocol's state; the session copies what the program asks for onto a queue for the main thread, andDndanswers it through the platform's drop target and drag source. A drop is accepted only once the program says it would take it, and its data is read down non-blocking pipes the loop waits on, a chunk a turn, so a drop from one pane to another is harrier at both ends of one pipe. For a program on another machine, it reads the files a drop names, a component at a time without following links, and writes out the files that program drags, likewise. On Windows, OLE gives a drop's data only while it is being dropped, so the platform takes all of it then, andDndreads it from memory a chunk a turn, the loop not waiting while there is more. A drag out is OLE'sDoDragDrop, which keeps the thread until the drop: it starts from a message posted to the window, so not in the middle of the app's handling of the program, and a drop target asking for data the program has not sent turns the app's loop from inside it until the program has, as a window's frame being dragged does. The files of a drag from another machine are written withstd.Iothere, each part of a path made a Windows name first, so that with no link ever made a path cannot lead out of the drag's directory.font/: what a terminal's cells want of zig-aerie's text.- The grid is a zig-aerie
Family: four styled fallback lists, and the cell, which is the family's line. Resolving a list sorts every font fontconfig knows, tens of milliseconds each with thousands installed; only the regular one is needed to open a window, so the other three are resolved on a thread meanwhile and waited for the first time bold or italic text wants a face. The scan and the regular list run on a thread of their own at startup, beside connecting to the compositor and setting up the GPU, and again when the configuration names another family, so that a font installed since is found. shape: one row at a time, with each cell's index as its cluster.cell_glyphs: a glyph out of zig-aerie's cache, fitted to the cells it spans, or one of harrier's own, drawn the first time it is asked for and kept in the same atlas.sprite/: the glyphs harrier draws itself.draw/andcanvas.zigare Ghostty'ssrc/font/sprite, drawing with z2d;Sprite.zigcollects thedraw<CP>anddraw<MIN>_<MAX>functions into a table of ranges at compile time, as Ghostty'sFace.zigdoes. The shaper hands such a cell over without shaping it.
- The grid is a zig-aerie
Testing #
Startup time #
HARRIER_STARTUP_TIMES=1 harrier prints how long each phase of starting
took, measured from when the process began, on standard error:
$ HARRIER_STARTUP_TIMES=1 harrier --standalone
startup: 4.1 ms (+ 0.2) configuration
startup: 6.7 ms (+ 2.6) window system
startup: 47.6 ms (+ 41.0) vulkan device
startup: 199.1 ms (+ 151.5) fonts: scan
startup: 257.3 ms (+ 58.2) fonts: the regular style resolved
startup: 274.7 ms (+ 7.1) first frame drawn
The fonts are most of it. Their scan reads zig-font-config's cache, one
file per font directory in ~/.cache/zig-font-config. A harrier built
with different font-scanning code does not trust another's cache, so the
first start after such an update, or after running a harrier of another
version, scans every font again: seconds, with thousands installed.
Tests #
zig build test covers:
- the pty and serial backends, against real pseudo-terminals;
- the title bar's room for the profile selector's button and the status button, only when there is one (its frame, insets and what is under the pointer are zig-aerie's, and tested there);
- the status button: its width as the count grows, and which state is the most pressing;
- resizing in whole cells: an offered size rounded down to the padding plus whole cells, at whole and fractional scales, without losing a cell to rounding;
- searching: matches found through the scrollback, newest first, and stepped through both ways with wrapping and scrolling; an empty query finding nothing; backspace taking off a whole character; a paste made one line;
- translations: every catalog parses cleanly, every action has a name, messages take their arguments, a missing message is its name, and the closest translation is chosen;
- a session end to end: a program's output, a device-attributes query answered back through the pty, titles, OSC 52, progress reports and their end, status reports kept as records with what to tell the user, keystrokes, resizes;
- program status records: a report replacing its record and telling of a change of state but not of more progress, children taking their program from their parents and cleared with them, a prompt ending what was going on, the order a tree shows them in and the lines that draw it, and the progress bar following the most recent report;
- a global hotkey's keysym and the trigger the portal is given for it, and global bindings kept apart from the window's own;
- configuration and key bindings, and
validate-config's checks; the desktop's settings as the configuration sees them; - the scrollbar: when it shows, the thumb's size and place, and the row a dragged thumb stands for;
- the split tree: where panes go, which a point or a direction finds, closing one, dragging and resizing dividers, equalizing and zooming, and a pane moved beside another, on the side its drop was nearest;
- the tab bar: when it shows, what is under the pointer as tabs multiply and narrow, and where a dragged tab goes as it passes its neighbours, and the status button making room for itself;
- the atlas, and shaping and rasterizing with whatever fonts the machine has;
- the built-in glyphs: every code point in every range draws, and lines and blocks land on the cell's edges;
- emoji presentation: Unicode's default for a character, and a variation selector overriding it;
- finding links: a URL from anywhere in it, the punctuation and brackets
around it left off, two in a row told apart, and which
file:URIs name files on this machine; - drag and drop: a directory's entries as the protocol lists them, a drag image's pixels, the list of a drag from another machine rewritten to name the copies, paths that would lead out of the copies' directory refused, and a dropped list of files as the quoted paths it is typed as;
- images: one sent to a terminal coming out placed, with its pixels to upload; a PNG decoded; an image sent again under its number placed where it was sent last; an animation moving on to its next frame when it says it will, with new pixels; pixels premultiplied for filtering; placements cut to the grid with their source cut to match; and the three layers by z-index.
The Wayland and Vulkan code is compiled by the tests, run in the VM test,
and exercised by running it. It has been run under a headless sway: typing, Compose, dead
keys, CJK fallback, 256-color and SGR rendering, dma-buf presentation with
explicit sync, a serial port through a socat pty pair, kitty images
(kitten icat's, and others sent every way the protocol allows, in each of
its layers, and cut where they scroll off the top), harrier's own
title bar and frame, both forced on sway and falling back on weston, tabs dragged
along the bar by a virtual pointer, panes dragged by their handles, the
status button in the title bar, the tab bar and as a badge, and the status
window it opens (floated by sway as a dialog, showing a clicked row's tab
while staying open), a floating window asked for 803×611 settling at
798×597 (79×31 cells and its padding), a search through 2,000 lines of
scrollback (Enter scrolling to the second-newest match, a common word's
1,993 matches all counted, the field clear of the status badge, and with
the field closed ctrl+shift+g twice landing on the second-newest match),
the profile selector (opened by a click on its ▾ in the title bar,
shown beside the tab bar's + too, narrowed by typing, ranked, saying
when nothing matches; a profile opened by a click on its row, one opened as a
tab, and ssh to a host it could not resolve staying open, with status 255,
to say so),
and the
built-in glyphs beside the
font's own. Drawing straight
into a dma-buf has been run on lavapipe (gpu = "llvmpipe"), whose images
sway imports, including a resize, with the validation layer clean. The RX 480
it was developed on has no modifier support, so there it copies.
nix flake check runs the package's tests in the sandbox, builds
harrier-terminfo (checking that both of the entry's names compile and
read back), and runs these NixOS VM tests:
sway: a window on a headless sway, with text in it; then a translucent one, half black over a red background coming out half red; then a program run withTERM=harrierfinding harrier's terminfo entry in harrier's own store path, through theTERMINFOharrier gives it; then, with Noto's monochrome and color emoji fonts both installed and fontconfig ranking the monochrome one first, 😀 drawn in color and 😀 followed by U+FE0E drawn without; then a 64×64 PNG sent with kitty's graphics protocol drawn pixel for pixel, and not at all with images turned off; then a profile opened from the profile selector by its key and a narrowing, in a window of its own with the profile's magenta background, its program given the profile'sTERM=xterm-256color, and a split of it running the profile again; then a program started in a systemd scope of its own with the configuredMemoryHighandTasksMax, harrier not in it, and the scope gone with the program; then the subtitle on harrier's own title bar, showing the directory/procfinds, then a directory under the home directory reported with OSC 7 and written~/…, then a host and a path, with the terminal's rows the same throughout, and the title alone with it turned off; then the bell rung in a hidden tab, marking the window's title until the tab is shown, and rung in a window without the focus, which sway then shows as urgent;activation: harrier installed by the NixOS module, its search provider linked where GNOME Shell reads it; two launches, one systemd user service started by the session bus, taking both windows;--tab, opening a tab in the last window rather than a window; GNOME Shell's search finding that tab, narrowing, its result's name and icon, and activating it to show it again, and a pane whose shell reports another machine found by that host's name, its location its description, and--focus-paneshowing a pane, and not one named for another primary; a profile found by its name, described as one, started in a tab when picked, and then listed after its pane; thenLaunchCommand: one window with a tab per command, named after the desktop entry, one held after its program exits and one saying its program does not exist, a bad desktop entry refused, and keys closing the held tabs and then the window; then progress on harrier's icon through Unity's launcher API, a tab at 40% and one with an error at 80% making 60% and urgent, asked withQueryand heard asUpdatesignals naming harrier's desktop file;tabs: tabs dragged by a virtual pointer (tests/nixos/drag.py, since a headless sway has no pointer of its own): one torn off into a window of its own, one moved onto another window's bar, and the window it left closing with its last tab; then splits, a pane opened right and below and the focused one closed, counted by the programs they run;splits: three panes, each filling itself with a color of its own, rearranged by dragging their handles with the virtual pointer — one dropped at another's left edge, one at another's bottom — with a screenshot showing which color is where after each, and a press away from any handle moving nothing;hotkeys: Hyprland, logged in on the console, binding global hotkeys with vicinae-hotkey-v1: ctrl+alt+t bound, and pressed on the machine's keyboard opening a window; a barexrefused, with Hyprland's reason in the log;shortcuts: KDE Plasma on Wayland, whose compositor has no hotkey protocol, so that the binding goes through the portal's GlobalShortcuts: the portal's dialog asking whether to add it, answered, the trigger the desktop reports, and the shortcut pressed opening a window;dnd: two windows side by side,kitten dndoffering a drag in one and taking drops in the other, and the virtual pointer dragging between them: text, then a directory tree with a symbolic link in it. Then the same tree to a kitten that, by bubblewrap and a different/etc/machine-id, is on another machine, so harrier sends it the files; and from such a kitten, so harrier writes its files out and names those. Last, files and then text dropped on a program that takes no drops, typed in as quoted paths and as text;portal: a real XDG desktop portal, with its GTK backend reading GSettings: the configuration chooses the background byctx.appearance.color_scheme, andgsettingschanging the color scheme while harrier runs changes the window's background. Then notifications, through the same portal to dunst: one from a tab behind another, titled with that tab's title and clicked, shows the tab, through harrier's bus name; and one from a second,--standaloneharrier, clicked, shows its tab, coming back through the portal to it and not to the first. Last, links: ctrl+click on a URL in the text, and on a hyperlink a program made to a file, each opened in the application the desktop opens such things with — here one that writes down what it was given — the URL without the word after it, the file as itself, and a click without ctrl opening nothing. Then program status: a program reporting working, more progress, waiting for permission and done, which dunst shows as three notifications, titled with the program and saying each;home-manager: the Home Manager module alone: the configuration written where harrier reads it, the service found through what the module linked, and a window drawn in the configured background.
config-rejected checks that a configuration with mistakes in it fails the
build that wants it, naming each one — one of them made only under the dark
color scheme, and said to be.
Not yet #
- Not drawn yet:
- a confirmation for pastes that contain a newline (they go through as xterm sends them).
- Platforms: the BSDs and macOS have no serial backend in zig-serial, and harrier has no macOS platform.
- On Windows: symbolic links a program on another machine drags. See Windows.
Where this lives #
The canonical repository is on my Forgejo instance, and Tangled and Radicle carry copies of it:
git clone https://git.jcollie.dev/jeff/harrier.git
-
Forgejo: https://git.jcollie.dev/jeff/harrier
-
Radicle:
rad:z3r6dNE7ufqG2YPr36axqqxwAZz4erad clone rad:z3r6dNE7ufqG2YPr36axqqxwAZz4e
CI runs on the Forgejo repository.
Report issues and send pull requests on the Tangled mirror, https://tangled.org/jcollie.dev/harrier. CONTRIBUTING.md says what a change needs before it goes in.
License #
MIT; see LICENSES/MIT.txt. The project follows the REUSE specification.
Everything in src/font/sprite/ but Sprite.zig
and font.zig are ported from Ghostty, © 2024 Mitchell Hashimoto and
Ghostty contributors, also MIT. So is how src/renderer/images.zig works
out where an image's placements go, and the terminfo entry in
dist/terminfo/harrier.terminfo.
The icon, in dist/icons, is cropped from Becky Matsubara's photograph
Circus hudsonius, male perched, Berkeley, California (2018),
licensed CC BY 2.0. It was scaled and converted to
PNG.
What harrier is built with, and those packages' own notices, is installed
beside it as share/licenses/harrier/THIRD-PARTY-NOTICES.txt, and on
Windows in the install's LICENSES folder. The build writes it
(tools/notices.zig) from the packages harrier's own code and C sources
come from, for the target built, so it lists what that build has in it; a
package with no notice it can find stops the build. Ghostty's vendored
simdutf, Zig's runtime and MinGW-w64's, whose notices are not in a
package, are in dist/notices.
References cited #
- Hashimoto, M., & Ghostty contributors. libghostty-vt, in Ghostty [Computer software]. https://github.com/ghostty-org/ghostty
- Ghostty contributors. Update to Zig 0.17 (Pull request #14519). GitHub. https://github.com/ghostty-org/ghostty/pull/14519
- Moy, T. XTerm Control Sequences. https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
- Goyal, K. The kitty keyboard protocol. https://sw.kovidgoyal.net/kitty/keyboard-protocol/
- Goyal, K. Copying all data types to the clipboard. kitty documentation. https://sw.kovidgoyal.net/kitty/clipboard/
- Craddock, N. zf [Computer software]. https://github.com/natecraddock/zf
- ConEmu contributors. ANSI X3.64 and Xterm-256 Support. ConEmu. https://conemu.github.io/en/AnsiEscapeCodes.html
- Brabant, A. xx-hotkey-v1 and vicinae-hotkey-v1 [Wayland protocols]. vicinae-wayland-protocols. https://github.com/vicinaehq/vicinae-wayland-protocols
- Flatpak contributors. org.freedesktop.portal.GlobalShortcuts. XDG Desktop Portal documentation. https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html
- freedesktop.org. Shortcuts Specification (Version 0.1, draft). https://specifications.freedesktop.org/shortcuts/latest/
- Superlogical. Program Status Protocol (OSC 7501). Rex documentation. https://www.superlogical.com/rex/docs/build/program-status
- Ubuntu contributors. Unity/LauncherAPI. Ubuntu Wiki, archived by the Internet Archive. https://web.archive.org/web/20231220063114/https://wiki.ubuntu.com/Unity/LauncherAPI
- Microsoft. Set the progress bar in the Windows Terminal. Microsoft Learn. https://learn.microsoft.com/en-us/windows/terminal/tutorials/progress-bar-sequences
- Microsoft. source command (winget). Microsoft Learn. https://learn.microsoft.com/en-us/windows/package-manager/winget/source
- The Khronos Group. Vulkan 1.4 Specification. https://registry.khronos.org/vulkan/specs/latest/html/vkspec.html
- The Khronos Group. SPIR-V Specification. https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html
- Snektron. vulkan-zig [Computer software]. https://github.com/Snektron/vulkan-zig
- The Wayland project. The Wayland Protocol. https://wayland.freedesktop.org/docs/book/
- The Wayland project. Wayland (Version 1.26.0) and wayland-protocols (Version 1.49) [Computer software]. freedesktop.org. https://gitlab.freedesktop.org/wayland/wayland-protocols
- The Linux kernel developers. Buffer Sharing and Synchronization (dma-buf). https://docs.kernel.org/driver-api/dma-buf.html
- The Linux kernel developers. drm_fourcc.h and input-event-codes.h. Linux kernel source. https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi
- The Linux man-pages project. ioctl_tty(2) and pts(4). Linux manual pages. https://man7.org/linux/man-pages/man2/ioctl_tty.2.html
- The xkbcommon project. libxkbcommon [Computer software] and The XKB keymap text format, V1. https://xkbcommon.org/doc/current/keymap-text-format-v1.html
- X.Org Foundation. keysymdef.h. xorgproto. https://gitlab.freedesktop.org/xorg/proto/xorgproto/-/blob/master/include/X11/keysymdef.h
- Microsoft. Clipboard Formats, HTML Clipboard Format and BITMAPV5HEADER structure. Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/dataxchg/clipboard-formats
- Microsoft. OpenType Specification. https://learn.microsoft.com/en-us/typography/opentype/spec/
- Fontconfig [Computer software]. freedesktop.org. https://gitlab.freedesktop.org/fontconfig/fontconfig
- freedesktop.org. XDG Base Directory Specification. https://specifications.freedesktop.org/basedir-spec/latest/
- ziglibs. known-folders [Computer software]. https://github.com/ziglibs/known-folders
- Ollie, J. C. zig-aerie [Computer software], harrier's window system, renderer and fonts. https://git.jcollie.dev/jeff/zig-aerie
- Ollie, J. C. zig-wayland-native, zig-xkb, zig-font, zig-font-config, zig-font-renderer, zig-font-shaper, struthio, zig-serial and zig-fluent [Computer software]. https://git.jcollie.dev/jeff
- Project Fluent. Fluent Syntax Guide. https://projectfluent.org/fluent/guide/
- Project Fluent. Fluent Syntax 1.0 [Specification]. https://github.com/projectfluent/fluent
- Matsubara, B. (2018). Circus hudsonius, male perched, Berkeley, California (cropped) [Photograph]. Wikimedia Commons. CC BY 2.0. https://commons.wikimedia.org/wiki/File:Circus_hudsonius,_male_perched,_Berkeley,_California_(cropped).jpg
- freedesktop.org. Desktop Entry Specification: D-Bus Activation. https://specifications.freedesktop.org/desktop-entry/latest/dbus.html
- Pennington, H., Carlsson, A., Larsson, A., Herzberg, S., McVittie, S., & Zeuthen, D. D-Bus Specification. freedesktop.org. https://dbus.freedesktop.org/doc/dbus-specification.html
- The systemd project. systemd.service — Service unit configuration. https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html
- The Wayland project. xdg-activation-v1. wayland-protocols. https://wayland.app/protocols/xdg-activation-v1
- Flatpak contributors. org.freedesktop.portal.Settings. XDG Desktop Portal documentation. https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Settings.html
- Flatpak contributors. org.freedesktop.portal.Notification. XDG Desktop Portal documentation. https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Notification.html
- The systemd project. org.freedesktop.systemd1 — The D-Bus interface of systemd. https://www.freedesktop.org/software/systemd/man/latest/org.freedesktop.systemd1.html
- The systemd project. systemd.resource-control — Resource control unit settings. https://www.freedesktop.org/software/systemd/man/latest/systemd.resource-control.html
- The systemd project. Desktop Environment Integration (application scope naming). https://systemd.io/DESKTOP_ENVIRONMENTS/
- Flatpak contributors. org.freedesktop.portal.OpenURI. XDG Desktop Portal documentation. https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.OpenURI.html
- Koblinger, E. Hyperlinks (a.k.a. HTML-like anchors) in terminal emulators. https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
- Flatpak contributors. org.freedesktop.host.portal.Registry. XDG Desktop Portal documentation. https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.host.portal.Registry.html
- dunst contributors. dunst (Version 1.13.2) [Computer software], the notification server the VM test clicks notifications in. https://github.com/dunst-project/dunst
- The Wayland project. ext-background-effect-v1. wayland-protocols. https://wayland.app/protocols/ext-background-effect-v1
- The GNOME Project. Writing a Search Provider. GNOME Developer Documentation. https://developer.gnome.org/documentation/tutorials/search-provider.html
- Davis, M., & Edberg, P. Unicode Emoji (Unicode Technical Standard #51). Unicode Consortium. https://www.unicode.org/reports/tr51/
- Sandlund, J. uucode [Computer software], whose copy of the Unicode Character Database zig-aerie reads emoji presentation from. https://github.com/jacobsandlund/uucode
- Goyal, K. The Drag and Drop protocol. kitty documentation. https://sw.kovidgoyal.net/kitty/dnd-protocol/
- Goyal, K. Terminal graphics protocol. kitty documentation. https://sw.kovidgoyal.net/kitty/graphics-protocol/
- Google. Wuffs: Wrangling Untrusted File Formats Safely [Computer software]. https://github.com/google/wuffs
- Goyal, K., & kitty contributors. kitty (Version 0.49.2) [Computer
software], whose
kitten dndthe tests drag and drop with, and whosekitten icatshows images. https://github.com/kovidgoyal/kitty - The GTK Team. gtk-shell.xml (GTK 4.22.4), the
gtk_shell1protocol between GTK and Mutter. https://gitlab.gnome.org/GNOME/gtk/-/blob/main/gdk/wayland/protocol/gtk-shell.xml - Brown, Z., Faure, D., McCombs, T., & Wick, S. (2026). Terminal Intent (Version 0.1, draft). freedesktop.org. https://specifications.freedesktop.org/terminal-intent/latest/
- freedesktop.org. Desktop Entry Specification. https://specifications.freedesktop.org/desktop-entry/latest/
- Home Manager contributors. Home Manager [Computer software]. https://github.com/nix-community/home-manager
- Ollie, J. C. zig-dbus-service [Computer software]. https://git.jcollie.dev/jeff/zig-dbus-service
- Culverhouse, T., & Ollie, J. C. mdbus [Computer software]. https://git.jcollie.dev/jeff/mdbus
- Zig Software Foundation. The Zig programming language [Computer software]. https://ziglang.org/
- Free Software Foundation Europe. REUSE Specification (Version 3.3). https://reuse.software/spec-3.3/
- Zig 89%
- Nix 11%
- Shell <1%
- Python <1%