wterm: The Ghostty-Powered Web Terminal from Vercel

BBetter Stack
Computing/SoftwareInternet Technology

Transcript

00:00:00This is Wterm, a web-based terminal emulator by Vasell
00:00:03that renders directly to the DOM instead of on a canvas.
00:00:06So text selection, browser find,
00:00:08and screen readers all just work with it.
00:00:10It's written in Zig, compiles to a 12 kilobyte WASM binary,
00:00:14and there's also an optional backend powered by LibGhosty,
00:00:17the same engine that powers the Ghosty terminal,
00:00:19which gives you full terminal compatibility in the browser.
00:00:22But is a 12 kilobyte WASM binary
00:00:24really enough to replace a native terminal emulator?
00:00:28Probably not, but hit subscribe and let's find out.
00:00:33So web terminals are pretty much everywhere.
00:00:36In cloud IDEs like GitHub Codespaces,
00:00:39infrastructure tools like Portainer or Qualify,
00:00:41and even desktop IDEs like VS Code or Cursor.
00:00:44But they all use Xterm.js to do it
00:00:47because it's been around for a long time
00:00:49and it's basically the default.
00:00:51Xterm has a big problem though.
00:00:52It renders to a canvas element.
00:00:54So doing something like selecting text
00:00:56or finding words in a page
00:00:58all have to be re-implemented from scratch,
00:01:00which doesn't always work that well.
00:01:02Wterm takes a completely different approach
00:01:04by rendering to the DOM,
00:01:05which means the terminal output is just HTML,
00:01:08and the browser basically handles that for free.
00:01:10But Wterm can also do some really cool things
00:01:13with this HTML rendering,
00:01:14like only re-render the row that has updated,
00:01:17instead of re-rendering the whole terminal on each frame.
00:01:20It can also be written in different frameworks
00:01:22like React or Vue.
00:01:23You can change the theme.
00:01:24And there's also a separate Wterm ghosty package,
00:01:27which swaps out the zig call with lib ghosty
00:01:29and works surprisingly better
00:01:31than the other web ghosty project,
00:01:33which we'll talk more about later on in the video.
00:01:35But for now, let's try out Wterm
00:01:37with a simple demo project.
00:01:38So after installing Wterm with React,
00:01:40I've imported the component as well as the CSS,
00:01:43and I'm rendering the component over here.
00:01:45So now if I run the app and then go to the browser,
00:01:48I can see that there is a terminal.
00:01:49But if I press a command like ls,
00:01:51we can see nothing happens.
00:01:52And this is because it's not connected
00:01:53to another computer to read information from.
00:01:56Let me explain.
00:01:57So right now the client isn't connected to a backend,
00:01:59so there's nowhere for it to get information from.
00:02:01But what we have to do is connect it to another machine.
00:02:04So it could be my local machine
00:02:06or a machine on the cloud,
00:02:07and then render a fake terminal or a pseudo terminal
00:02:10inside that machine with the same dimensions
00:02:13as the one from the client.
00:02:14So if we do some keystrokes,
00:02:16that keystroke information gets sent
00:02:18to the other machine,
00:02:20which executes those keystrokes,
00:02:22renders the results,
00:02:23and sends all that information back to the client.
00:02:25And that back and forth needs to happen
00:02:26very quickly with minimal delay.
00:02:28So the best way to connect the client
00:02:30and the other machine together
00:02:31is to use WebSockets.
00:02:32So let's go ahead and do that.
00:02:34So we can use a HETS in the server
00:02:35with Ubuntu I already have set up
00:02:37with Node installed.
00:02:38And I also already have a Wterm server
00:02:40with a server script.
00:02:42So if we look into that,
00:02:43we can see we're creating a WebSocket server
00:02:45on the slash API terminal path.
00:02:48This will make more sense a bit later.
00:02:49And down here,
00:02:49we're spawning a pseudo terminal
00:02:51with a name that matches our terminal type.
00:02:53Here's how you can find yours if you're curious.
00:02:55And down here,
00:02:56we're getting any keystroke from the client,
00:02:58processing it on the server,
00:02:59so inside our fake terminal,
00:03:01and then returning that information
00:03:02to the client over here.
00:03:03So the server returns everything
00:03:05and not just a specific row that's been updated.
00:03:07Now over on the client in the app.tsx file,
00:03:10we're making a WebSocket connection
00:03:11to our server on the slash API slash terminal port.
00:03:14Then we're using the WebSocket transform
00:03:16from Wterm to connect to that URL
00:03:19with automatic reconnects.
00:03:21Then this is what sends keystroke information
00:03:23from here to the server.
00:03:24We handle browser resize here,
00:03:26and then down here,
00:03:27and the handle data function
00:03:28handles all the information
00:03:30that comes from the server.
00:03:31And the cool thing about the ZIG core
00:03:33is that it will pass this information,
00:03:35figures out what's been changed,
00:03:36and only re-renders that part of the HTML.
00:03:39Down here, the column and row size
00:03:41need to match what we had on the server,
00:03:42and everything else is pretty self-explanatory.
00:03:45So now with the client and server running,
00:03:47back in the browser, if I press LS,
00:03:49we can see it lists the files we have available.
00:03:52So I could press LS with the L flag
00:03:53to see more information about the files.
00:03:55I can CD into a file,
00:03:57take a look at the information inside it,
00:03:59and also do things like see the list
00:04:01of containers I have running.
00:04:02I can even open a file with Vim
00:04:03and navigate through it.
00:04:04But even though all of that works,
00:04:06it doesn't do it amazingly well.
00:04:07I mean, if we try to highlight some text,
00:04:09we can see some characters
00:04:10are completely unreadable.
00:04:12So to fix that,
00:04:13we can set up Wterm with Ghosty
00:04:15by loading Ghosty core
00:04:16and adding it as a prop in React.
00:04:18You can see now that if we open the server file
00:04:20and highlight some text,
00:04:22everything is much more readable.
00:04:23It can even do things like
00:04:24render open code correctly,
00:04:26allowing us to change models,
00:04:27and give it a prompt with emoji support.
00:04:29We can even see that Ghosty renders colors
00:04:31slightly better
00:04:31than with the default Wterm core renderer.
00:04:34But the Zig core is only 12 kilobytes
00:04:36compared to the 400 kilobytes from Ghosty.
00:04:39So if you care about size,
00:04:40then maybe stick to the Zig core.
00:04:43Anyway, that's a quick overview of Wterm from Vercel.
00:04:46Of course, there are so many more features
00:04:48I didn't go through,
00:04:49like being able to convert Markdown
00:04:51to a nice terminal output,
00:04:52using just Bash to navigate through fake files
00:04:55if you don't have access to a backend.
00:04:57And there are even examples
00:04:58of how to set up an SSH client
00:05:00through a terminal in the browser.
00:05:02But I didn't find Wterm to be perfect.
00:05:05There were some rendering issues
00:05:06when using the Ghosty version,
00:05:08going back and forth between NeoVim
00:05:10or even OpenCode.
00:05:11And to get the Ghosty renderer to work
00:05:13with my BUN frontend,
00:05:15I had to import the WASM file
00:05:17because BUN wouldn't copy any non-JS files
00:05:19from the Node modules folder.
00:05:21But I do like the DOM rendering approach,
00:05:23which means you do get accessibility
00:05:25and native browser features
00:05:27without doing any extra work,
00:05:29which Xterm has struggled to do,
00:05:31even though it's been around for more than 10 years.
00:05:33But Xterm.js does have a massive ecosystem
00:05:35and is the battle-tested solution,
00:05:38so you won't go wrong if you do end up choosing it.
00:05:40There's also GhostyWeb by Coda,
00:05:42which takes a different approach.
00:05:43It uses the same libGhosty engine
00:05:45used by the actual Ghosty terminal,
00:05:48but it's a drop-in replacement for Xterm,
00:05:50so it still uses the canvas rendering approach
00:05:52and uses the same API,
00:05:54but you do get a better terminal.

Key Takeaway

Wterm offers a lightweight, DOM-rendered terminal emulator that provides native browser accessibility features while maintaining terminal compatibility through an optional LibGhosty backend.

Highlights

  • Wterm uses DOM-based rendering instead of canvas, allowing native browser functionality like text selection and screen reader support.

  • The core Zig implementation compiles to a 12 KB WASM binary, significantly smaller than the 400 KB Ghosty core.

  • Wterm improves rendering efficiency by updating only the specific row that changed rather than re-rendering the entire terminal frame.

  • WebSocket integration enables real-time communication between the browser client and a backend pseudo-terminal for command execution.

  • Integrating the optional LibGhosty engine improves terminal color rendering and overall compatibility at the cost of larger binary size.

  • Unlike the canvas-based Xterm.js, Wterm treats terminal output as standard HTML, making it natively compatible with browser find and selection features.

Timeline

Wterm DOM Rendering Architecture

  • Wterm renders terminal output directly to the DOM to leverage native browser text selection and search.
  • The project utilizes a 12 KB WASM binary written in Zig for its core functionality.
  • Canvas-based terminals like Xterm.js require custom implementations for standard features like text selection and find.
  • Performance is optimized by re-rendering only the individual rows that change during operations.

Web terminals traditionally rely on Xterm.js and canvas elements, which necessitates re-implementing basic browser capabilities from scratch. By rendering to the DOM, Wterm treats terminal output as HTML, ensuring that browser features work out of the box. The architecture allows for modularity, supporting frameworks like React and Vue, and offers an optional LibGhosty backend for full terminal compatibility.

Client-Server Integration with WebSockets

  • Browser-based terminals require a backend connection via WebSockets to execute commands.
  • The server spawns a pseudo-terminal that matches the dimensions and state of the client terminal.
  • Keystroke data transfers to the server for processing, with results sent back to the client for rendering.

Without a backend, a terminal component in the browser cannot execute commands like ls. Establishing a WebSocket server on a path such as /api/terminal allows the client to send user keystrokes to a machine—local or cloud—which processes them in a pseudo-terminal and returns the output. The Wterm client handles data streams and resizes while maintaining a responsive display.

Advanced Rendering and Comparison

  • LibGhosty improves rendering quality for complex terminal outputs like Vim or LLM-based prompts.
  • The LibGhosty engine increases the binary size to 400 KB compared to the 12 KB base Zig core.
  • Xterm.js remains the battle-tested standard with a massive ecosystem despite rendering limitations.
  • GhostyWeb serves as an alternative that retains the Xterm.js canvas rendering approach while providing the improved LibGhosty engine.

While the default Zig core is highly compact, it may suffer from character rendering issues in complex environments like NeoVim. Swapping to the LibGhosty engine fixes these readability issues and enhances color rendering. Users must choose between the extreme efficiency of the 12 KB Zig binary or the comprehensive compatibility of the 400 KB Ghosty-powered alternative.

Community Posts

No posts yet. Be the first to write about this video!

Write about this video