Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -3184,7 +3184,8 @@ Alert rows of the table above from the `--droplet-*` roles in `src/styles.css`.
`composerPasteSend`), and on a touch screen the suggestion chip (off by default); then
**Quick replies**; then **File viewer**: wrap long lines, Markdown width, Diff layout), **Terminal** (font size, font family, wheel
speed, input mode, Key bar, Clipboard from a pane, Pictures in the terminal (off by default, the
device setting `terminalImages`), Use alongside herdr's own window, Follow
device setting `terminalImages`), Draw with libghostty (off by default, the device setting
`terminalGhostty`), Use alongside herdr's own window, Follow
herdr's focus), **Alerts**, **Voice input**, **Agents**,
**Shortcuts** (the platform-resolved global bindings), **Phone & devices** (Connect a phone,
Keep screen on, Install; then Portal, where the server offers it; then paired devices; then Access),
Expand Down Expand Up @@ -3483,6 +3484,13 @@ Alert rows of the table above from the `--droplet-*` roles in `src/styles.css`.
ordered change set, so Retry resends the current change, never an old request body.

### Terminal host, key bar and drawer
- **Draw with libghostty** (Settings > Terminal, off by default, herdr-core only): libghostty-vt draws the
pane's text, cursor and Kitty graphics pictures in a layer over xterm's screen, at xterm's cell size;
xterm's own text and cursor are hidden under it and xterm keeps input, selection, links, search and
scrolling. It follows Ghostty's own split of a terminal core (libghostty-vt) from the renderer an app
draws with, rather than a second terminal widget: everything built on xterm keeps working while the
drawing moves. The layer takes no pointer events; pictures keep the layer Kitty gave them (below
the text, or above it).
- **Find in terminal** in the header's More menu and Mod+Shift+F opens a find bar above the
grid. It uses the existing input and icon-button primitives: a query, match ordinal,
previous/next and close controls, with a dim note that scrolling is shared by all clients.
Expand Down
99 changes: 99 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -1518,6 +1518,105 @@ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
```

## libghostty-vt

`src/lib/ghostty/ghostty-vt.wasm` is libghostty-vt from [Ghostty](https://github.com/ghostty-org/ghostty) at
commit e95bed6, built for wasm32 by `scripts/ghostty-vt/build.sh` with the small change in
`scripts/ghostty-vt/kitty-on-wasm.patch` (Kitty graphics on, file media off). It draws a terminal when
Settings > Terminal > Draw with libghostty is on, under the MIT License below. The module links two
packages Ghostty's build brings in: [Wuffs](https://github.com/google/wuffs) 0.4.0-alpha.10, which decodes
Kitty PNG pictures, under the MIT License (it is offered under MIT or Apache-2.0), and
[uucode](https://github.com/jacobsandlund/uucode) 0.2.0, Unicode tables, under the MIT License.

```
MIT License

Copyright (c) 2024 Mitchell Hashimoto, Ghostty contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```

Wuffs:

```
Copyright 2023 The Wuffs Authors

Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
```

uucode:

```
# uucode license

MIT License

Copyright (c) 2026 Jacob Sandlund

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

## Other licenses

See [./licenses](./licenses) for licenses of code being used in the repo:

* [LICENSE_Bjoern_Hoehrmann](./licenses/LICENSE_Bjoern_Hoehrmann)
* [LICENSE_unicode](./licenses/LICENSE_unicode)
```

## JetBrains Mono

`src/fonts/JetBrainsMono-Regular.woff2` and `src/fonts/JetBrainsMono-Bold.woff2` are
Expand Down
5 changes: 5 additions & 0 deletions changelog.d/terminal-ghostty-renderer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### Added
- Settings > Terminal > Draw with libghostty (off by default) draws a pane with libghostty, the
terminal engine of Ghostty, on a PC that runs herdr-core, as the desktop app does, with the pictures
a program sends with the Kitty graphics protocol (kitty icat, timg, chafa). Typing, selection,
links and search still go through xterm.js underneath.
1 change: 1 addition & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ bun scripts/pane-input-focus-demo-regression.ts # the input focus shortcut again
bun scripts/mirror-scroll-demo-regression.ts # fixed mirror wheel/touch, native history, no guessed program input, observe guards
bun scripts/terminal-images-demo-regression.ts # Settings -> Pictures in the terminal: a picture is drawn, goes while off, and on again the next one is drawn only where it was printed
bun scripts/core-history-scroll-demo-regression.ts # a herdr-core server's history kept in xterm: wheel and swipe scroll it, a replay erases the last one, nothing typed; herdr's own keeps none (#2663)
bun scripts/terminal-ghostty-demo-regression.ts # Settings -> Draw with libghostty: a herdr-core pane drawn from its first byte, a Kitty picture placed and deleted, typing still through xterm; herdr's own and off stay xterm without fetching the module
bun scripts/mirror-fold-demo-regression.ts # a mirror, or an attached pane a phone watches at a wider grid, folded whole (portrait, landscape), Original width from the pane menu, driving phones and desktops unfolded
bun scripts/shortcut-exact-modifiers-demo-regression.ts # recording exact-modifier chords: confirm, refusals, dispatch
bun scripts/output-browser-qa.ts # terminal output flow control end to end
Expand Down
1 change: 1 addition & 0 deletions scripts/ci-browser.sh
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ run_script scripts/omo-question-wheel-regression.ts
run_script scripts/mirror-scroll-demo-regression.ts
run_script scripts/core-history-scroll-demo-regression.ts
run_script scripts/terminal-images-demo-regression.ts
run_script scripts/terminal-ghostty-demo-regression.ts
run_script scripts/mirror-fold-demo-regression.ts
run_script scripts/chat-no-rewind-demo-regression.ts
run_script scripts/terminal-cjk-punctuation-regression.ts
Expand Down
2 changes: 1 addition & 1 deletion scripts/compressed-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const brotli = promisify(brotliCompress), gz = promisify(gzip);
export function compressedAssets(): Plugin {
return { name: "compressed-static-assets", async writeBundle(options, bundle) {
if (!options.dir) return;
await Promise.all(Object.values(bundle).filter(asset => /\.(?:js|css)$/.test(asset.fileName)).map(async asset => {
await Promise.all(Object.values(bundle).filter(asset => /\.(?:js|css|wasm)$/.test(asset.fileName)).map(async asset => {
const source = asset.type === "chunk" ? asset.code : asset.source;
const bytes = typeof source === "string" ? Buffer.from(source) : Buffer.from(source);
const [br, gzip] = await Promise.all([brotli(bytes, { params: { [constants.BROTLI_PARAM_QUALITY]: 5 } }), gz(bytes, { level: 9 })]);
Expand Down
25 changes: 25 additions & 0 deletions scripts/ghostty-vt/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# Rebuilds src/lib/ghostty/ghostty-vt.wasm: libghostty-vt from ghostty (MIT) at a pinned commit, with
# kitty-on-wasm.patch, which turns the Kitty graphics protocol on for wasm32-freestanding (upstream
# leaves it off there: no file medium and no PNG decoder). The patch refuses the file, temporary
# file and shared memory media, which have no meaning in a browser, and links ghostty's bundled
# wuffs PNG decoder, so a PNG a program sends is decoded inside the module.
#
# ZIG=/path/to/zig-0.17.1/zig scripts/ghostty-vt/build.sh
#
# Expected output sha256 (zig 0.17.1, x86_64-linux):
# b8e8d0f8dd4e84718002198f80d5399005d6d42f2d419b31644343d5703a1444
set -euo pipefail
GHOSTTY_COMMIT=e95bed6
here="$(cd "$(dirname "$0")" && pwd)"
repo="$(cd "$here/../.." && pwd)"
zig="${ZIG:-zig}"
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
git -C "$work" init -q ghostty
git -C "$work/ghostty" fetch -q --depth 1 https://github.com/ghostty-org/ghostty "$GHOSTTY_COMMIT"
git -C "$work/ghostty" checkout -q FETCH_HEAD
git -C "$work/ghostty" apply "$here/kitty-on-wasm.patch"
(cd "$work/ghostty" && "$zig" build -Demit-lib-vt -Dtarget=wasm32-freestanding -Doptimize=ReleaseSmall --prefix "$work/out")
cp "$work/out/bin/ghostty-vt.wasm" "$repo/src/lib/ghostty/ghostty-vt.wasm"
sha256sum "$repo/src/lib/ghostty/ghostty-vt.wasm"
40 changes: 40 additions & 0 deletions scripts/ghostty-vt/kitty-on-wasm.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
diff --git a/src/terminal/build_options.zig b/src/terminal/build_options.zig
index 628ecbd..1542c32 100644
--- a/src/terminal/build_options.zig
+++ b/src/terminal/build_options.zig
@@ -272,7 +272,7 @@ pub const Options = struct {
/// targets, so it is always disabled there regardless of the
/// feature setting.
pub fn kittyGraphics(self: Options, target: std.Target) bool {
- if (target.os.tag == .freestanding) return false;
+ _ = target;
return self.features.kitty_graphics;
}

diff --git a/src/terminal/kitty/graphics_image.zig b/src/terminal/kitty/graphics_image.zig
index 281bb02..cba04a5 100644
--- a/src/terminal/kitty/graphics_image.zig
+++ b/src/terminal/kitty/graphics_image.zig
@@ -182,6 +182,9 @@ pub const LoadingImage = struct {
}
}

+ // Freestanding (wasm) has no filesystem: only direct payloads.
+ if (comptime builtin.target.os.tag == .freestanding) return error.UnsupportedMedium;
+
// Depending on the medium, load the data from the path.
switch (t.medium) {
.direct => unreachable, // handled above
diff --git a/src/terminal/sys.zig b/src/terminal/sys.zig
index 3f6b501..712a754 100644
--- a/src/terminal/sys.zig
+++ b/src/terminal/sys.zig
@@ -16,7 +16,7 @@ const build_options = @import("terminal_options");
/// and the exact semantics are up to callers. For example, the Kitty Graphics
/// Protocol will work but cannot accept PNG images.
pub var decode_png: ?DecodePngFn = png: {
- if (build_options.artifact == .lib) break :png null;
+ if (build_options.artifact == .lib and @import("builtin").target.os.tag != .freestanding) break :png null;
break :png &decodePngWuffs;
};

Loading
Loading