terminal-fenster
Health Uyari
- License — License: MIT
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Low visibility — Only 6 GitHub stars
Code Uyari
- fs module — File system access in apps/engine/spike/b02-capability-probe.js
- fs module — File system access in apps/engine/spike/fps-matrix.js
Permissions Gecti
- Permissions — No dangerous permissions requested
Bu listing icin henuz AI raporu yok.
Real Chromium browser in your terminal
Terminal-Fenster
A real Chromium browser that renders as pixels inside your terminal — not a text-mode
approximation, not a screenshot viewer. Chromium 150 renders offscreen; a Rust core turns
frames into terminal graphics and turns your keyboard and mouse back into browser input.
terminal-fenster open news.ycombinator.com

Real Ghostty capture on macOS: Chromium pixels rendered through the Kitty graphics protocol,
with terminal keyboard and mouse input forwarded back to the page.
Open source
Licensed under MIT. Contributions welcome — see CONTRIBUTING.md.
Source: github.com/zent7x/terminal-fenster ·
commit history ·
landing-page source
This is experimental software: see Known gaps and RELEASE.md before
treating it as production-ready.
Status: working vertical slice, not shippable yet
This is honest about where it is. What follows is measured, not aspirational.
Verified working
- Real pages render in Ghostty via the Kitty graphics protocol, auto-detected.
- Click, hover, ordered typing, scroll, resize, focus pacing, and idle wake-up all work —
verified with real frame pixels, page state, and scheduler state
(tests/e2e/input-injection.js, 20/20). - The terminal is restored on exit,
ctrl+q, panic, SIGINT/SIGTERM/SIGHUP. - The headless engine probe now asserts process-tree cleanup; its reference
about:blank
upper-bound RSS is about 281 MB with no surviving Chromium helpers. - Renderer crashes produce a visible, escape-sanitized reload banner instead of a silent freeze.
- Interactive sessions support up to 16 tabs with a terminal-native tab strip; new, switch,
close, and preserved background-tab state are covered by the real-pixel E2E suite. - A 16-tool MCP server drives an isolated Terminal-Fenster engine through accessibility refs and
the private engine socket; its real-Chromium suite passes 28/28. - 202 Rust checks, 14 engine unit tests, 20 engine E2E checks, 14 browser fixtures,
14 MCP compositor/discovery/privacy tests, 26 MCP protocol checks, and 28 live MCP checks.
Not done — see Known gaps and RELEASE.md.
Requirements
- macOS or Linux. Developed and measured on macOS 26.1 / Apple M4.
- A terminal with the Kitty graphics protocol for full fidelity: Ghostty, kitty, or
WezTerm. iTerm2 also speaks it. A terminal with no graphics protocol at all (e.g.
Apple Terminal) is headless-only — interactiveopenrefuses it and points you to--headless; sixel- or iTerm2-only terminals render interactively through a low-fidelity
Unicode half-block fallback. - Rust 1.80+, Node 22.12+ (required by the pinned Electron runtime).
Verification scope: Ghostty on macOS is the only interactive configuration verified
end-to-end today. Kitty and WezTerm should work through the same protocol but need community
testing, and Linux has automated build/layout coverage rather than a live graphics-terminal run.
There are no signed or notarized binary releases yet.
Install from a checkout
./install.sh
The source installer builds with the lockfile, stages and materializes Electron before touching
the active install, validates the pinned runtime, then atomically swaps~/.local/share/terminal-fenster and links ~/.local/bin/terminal-fenster. A failed upgrade restores the
previous prefix. Set TERMINAL_FENSTER_REUSE_ENGINE=1 to reuse this checkout's already-materialized
engine for an offline/same-machine install. Until a signed release tag exists, do not present thecurl | bash form as a reproducible binary release.
./uninstall.sh # preserve browser profiles
./uninstall.sh --purge-profile # also permanently remove Terminal-Fenster profiles
The uninstaller refuses unmarked/unrelated prefixes and never removes the shared Electron cache.
Build a prebuilt release archive
tools/package-release.sh
This produces a host-native .tar.gz and SHA-256 sidecar under dist/. The archive contains a
minimal engine launcher and the pinned Electron runtime, so installing it needs neither Rust,
Node, npm, a source checkout, nor network access. The packager independently verifies Electron
and its free-codecs FFmpeg artifact, includes all Cargo/Electron/Chromium license material,
checks every extracted payload file, then installs and launches from the archive layout. On
macOS the local artifact is ad-hoc signed for structural verification only; it is not a public
release until Developer-ID signing and notarization pass the gate in RELEASE.md.
The Unsigned release candidates workflow runs the same native packager on macOS arm64/x64 and
Linux arm64/x64 for an exact version tag or manual dispatch. It retains archives and sidecars as
short-lived CI artifacts only; it deliberately cannot publish a GitHub release or turn an ad-hoc
macOS signature into a production signature.
Quick start
./install.sh # one-shot install (Rust + Node required once)
terminal-fenster setup # verify engine + terminal, get next steps
terminal-fenster open example.com # interactive (Ghostty / kitty / WezTerm / iTerm2)
terminal-fenster open example.com --headless # scripts, CI, agents — any terminal
macOS Terminal.app cannot render pages (no graphics protocol). Use a Kitty-capable
terminal for interactive browsing, or --headless everywhere else.
Build and run
git clone https://github.com/zent7x/terminal-fenster.git
cd terminal-fenster
cargo build --release # builds the terminal core
cd apps/engine && npm ci # installs the pinned Electron package
./node_modules/.bin/electron --version # materializes Chromium (~300 MB); required once
cd ../..
./target/release/terminal-fenster doctor # what your terminal can do
./target/release/terminal-fenster open example.com
doctor first. It tells you which backend you get and, more usefully, why, including the
raw bytes your terminal replied with:
graphics
kitty graphics yes
sixel no
--> backend kitty
input
kitty keyboard yes
sgr-pixels mouse yes
geometry
cells 146x23
cell px 17x37
page viewport 2482x851
Keys
| Key | Action |
|---|---|
ctrl+q |
quit |
ctrl+c |
copy selection |
ctrl+r |
reload |
ctrl+l |
open URL (omnibox on the status row) |
ctrl+f |
find in page (ctrl+n / ctrl+p next/prev) |
ctrl+= / ctrl+- / ctrl+0 |
zoom in / out / reset |
ctrl+left / ctrl+right |
back / forward (alt+arrow where the terminal delivers it) |
| mouse | click, hover, drag, scroll — forwarded to the page |
MCP automation
Terminal-Fenster ships a stdio MCP server with 16 browser tools (navigate, snapshot, click,
type, screenshot, …). Any MCP client that launches subprocess servers over stdio can use it.
./install.sh
terminal-fenster mcp-config # JSON for your client's mcpServers block
terminal-fenster mcp # run the server directly
Paste the JSON into your editor or harness MCP settings. The command must be the installedterminal-fenster binary; args is ["mcp"].
Full tool list, security model, and env vars: packages/mcp/README.md.
Automation details
packages/mcp/ starts an isolated engine and exposes 16 browser tools over
stdio: navigate, accessibility snapshot/find, click/type/key/scroll, screenshots, history,
resize, wait, status, and close.
Semantic inspection uses Electron's in-process debugger API proxied through the existing 0600
Unix socket; it opens no DevTools TCP port. See packages/mcp/README.md
for client configuration.
How it works
your terminal
│ tty in raw mode, owned by an RAII guard that restores it on every exit path
[terminal-fenster] Rust: capability detection, compositor, input decoding, Kitty encoding
│ unix socket, 0600, inside a 0700 dir — no network listener is ever opened
[engine host] Electron: offscreen Chromium, sandbox ON
Frames are raw BGRA from Chromium's paint event. The core retains an RGB canvas and uses a
hybrid Kitty compositor: a runtime-probed POSIX shared-memory base for dense local repaints,
and zlib-compressed position-bound tile overlays for sparse damage. SSH, multiplexers, a failed
probe, or four unconsumed shared objects fall back to direct/zlib transmission. Commands and
events are JSON on the private socket; frames are binary and length-prefixed because a 5 MB
bitmap through JSON would be indefensible.
Architecture decisions with their measurements and their reversal costs live indocs/adr/. ADR-0001 records the engine choice and the argument it was
originally defended with, which later measurement proved false.
Measured performance
Ghostty 1.3.1, macOS 26.1, Apple M4 (10 cores, 24 GB), release build. These are real
core-to-terminal runs, not engine-only paint rates:
| Workload | Viewport | Steady received FPS | Trailing FPS | Final image wire | Encode |
|---|---|---|---|---|---|
| Local 80×80 animation, default 4×4-cell mosaic | 2108×1332 | 46.5 | 54 | 2,540 B | 0.18 ms |
| Full-viewport repaint, direct/zlib monolithic baseline | 2108×1406 | 7.2 | 7 | 82,086 B | 2.68 ms |
The localized workload is the intended fast path: Chromium sends damage, the core updates a
retained RGB canvas, and only intersecting Kitty tiles cross the terminal boundary. The
direct full-viewport baseline shows the old bottleneck honestly: encoding is only 2.68 ms, but
base64/APC terminal presentation held throughput near 7 fps. The current build runtime-probes
Kitty shared memory and keeps an 8,891,544-byte 2108×1406 RGB frame behind a sub-160-byte PTY
command in OS-level tests; its real Ghostty FPS is deliberately not claimed until the release
gate is rerun. The older logs above count engine frames received; benchmark schema v2 now also
records completed presentations plus BGRA conversion, encode, wire, and presentation-gap p50/p99
so coalescing or hidden conversion work cannot inflate future results.
Reproduce with benchmarks/, or directly:
node benchmarks/bench.mjs --page local-damage
TERMINAL_FENSTER_SHM=0 TERMINAL_FENSTER_TILE_CELLS=1x1 \
node benchmarks/bench.mjs --page repaint --label direct-control
Terminal support
| Terminal | Graphics | Kitty keyboard | Pixel mouse | Status |
|---|---|---|---|---|
| Ghostty 1.3.1 | Kitty | yes | yes | verified end-to-end |
| iTerm2 3.6.9 | Kitty | yes | no (permanently reset) | protocol-verified, app not driven |
| Apple Terminal 465 | none → headless-only | no | no | capability-verified |
| kitty, WezTerm | Kitty | yes | yes | expected, untested |
Every "yes" above comes from the terminal answering a protocol query, not from matching$TERM. Detection is in crates/tf-term/src/caps.rs.
Note the iTerm2 row: it reports SGR-Pixels mouse mode permanently reset, so coordinates
arrive as cells. Treating those as pixels would collapse the whole page into its top-left
corner — the pointer mapping handles both, with tests pinning the difference.
Testing
cargo test --workspace --locked # 202 Rust checks, no terminal needed
cd apps/engine && npm test # 14 frame/tab/security unit tests
cd ../.. && node tests/e2e/input-injection.js # 20 real-pixel / page / security checks
apps/engine/node_modules/.bin/electron tests/fixtures/verify-fixtures.js # 14 fixtures
cd packages/mcp && npm test # 14 compositor/discovery/privacy + 26 protocol checks
npm run test:live # 28 tools against real Chromium
tools/package-layout-test.sh # manifest tamper + prebuilt install/layout smoke test
The e2e test speaks the engine wire protocol directly, so it runs without a graphics
terminal and works in CI. It asserts on frame pixels: a click must actually change the
colour under the cursor, and clicking one target must not activate another.
Known gaps
Ordered by how much they matter.
- The hybrid compositor still needs eyes on a graphics terminal. Capture-side damage,
retained RGB, the dense-base/sparse-overlay switch, exact encoder round trips, real POSIX
shared-memory objects, stale-overlay deletion, and bounded fallback are tested. On-screen
placement and teardown after switching between shared bases and tile overlays still need a
Ghostty run before release. - Full-viewport motion needs a fresh measurement. The direct fallback measured about
7 fps at 2108×1406. The new localt=spath removes pixels, compression, base64, and APC
chunking from the PTY, but no honest displayed-FPS number exists yet.RELEASE.mdmakes
20 displayed fps and a 2× improvement over direct control the go/no-go threshold. - SSH adaptive transport: MBDT, byte-credit, fps ladder, and OSR scale downs are wired
on the direct Kitty path ({"t":"fps"}+ resize + pointer remap + Kittyc/rstretch).
WAN measurement still open;TERMINAL_FENSTER_LAG_BUDGET_MS(default 100) tunes the credit
window. - Tabs are intentionally basic. The interactive CLI has a tab strip and supports up to
16 live tabs, but has no persisted tab restore, bookmarks, or richer tab management. MCP
sessions remain intentionally single-tab and isolated from interactive sessions. - Sixel and iTerm2 backends are unimplemented. If detection picks one, the CLI
explicitly degrades to Unicode anddoctorsays so rather than silently faking it. onPaintstill copies viatoBitmap()— B04's finding; shared-texture path is not
shipped.- Public artifacts are not signed or notarized. The macOS arm64 archive pipeline is proven
locally, but other target archives and a genuinely clean-machine install remain release gates. - Terminal coverage is narrow. Ghostty on macOS is verified end-to-end. Kitty, WezTerm,
Linux interactive rendering, and the iTerm2 fallback need community testing before their
support can be promoted beyond expected/protocol-verified. - The Electron memory floor is still high. A short 1280×800
about:blankprobe measured
280.6 MB peak/steady summed RSS (an upper bound because shared pages are double-counted). The
idle frame throttle works, but a separately verified low-memory mode is not shipped.
Security posture
- Chromium's sandbox stays on; web content gets no Node integration and context
isolation is enforced. - Camera, microphone, location, clipboard, device, and every other privileged page permission
are denied by explicit session handlers. External-application URL schemes are blocked before
navigation; denied permissions, schemes, and popups produce a sanitized terminal notice. - Page-derived text (titles, URLs) is sanitized before it can reach the terminal — a
malicious title otherwise smuggles escape sequences through us and can drive OSC 52 to
overwrite your clipboard. C0/C1 controls, bidi overrides/isolates, and invisible formatting
characters are stripped; the final chrome line is column-clipped before the autowrap cell. - The control socket is 0600 inside a 0700 directory. MCP CDP calls use Electron's
in-process debugger through that socket; no DevTools TCP endpoint is exposed. - Initial URLs travel over that socket rather than process arguments. Diagnostic and MCP audit
logs are 0600, refuse symlinks, and structurally redact URL secrets; agent snapshots redact
every editable value and agent navigation cannot open local files, blobs, or custom schemes. - Shared-memory frame objects are 0600, carry unique collision-checked names, are unlinked by
the terminal or our guard, and are capped at four outstanding objects before direct fallback. - The frame reader caps message size, so a bad length prefix cannot turn into a 4 GiB
allocation.
Run tools/release-check.sh, then close the manual terminal and
distribution gates in RELEASE.md before tagging anything.
See SECURITY.md for reporting vulnerabilities.
License
MIT — see LICENSE. Third-party components and prior art studied (but not copied) are
listed in NOTICE.md. Notably the benchmark product, zenbu-labs/terminal-browser, ships
no licence file, so its implementation was treated as unavailable: only its public
behaviour informed this work.
Yorumlar (0)
Yorum birakmak icin giris yap.
Yorum birakSonuc bulunamadi