Madrid
Meridan ReportMadrid: Spain's digital transformation, European expansion

Advertisement Β· Why this ad?

More local news across the network β†’
Technology

Terminal borders that don't cost a cell

Marcus Chen
Managing Editor Β· Friday, November 13, 2026 at 11:06 PM Β· 7 min read
Terminal borders that don't cost a cell β€” Madrid Technology news

Terminal borders that don't cost a cell 2026-11-13 :: tags: #Ghostty #Ratatui #TUI #Terminals This post (and the code behind it) was written with a lot of help from AI. I'm not going to apologize for that. If that bothers you, feel free to close the tab. I maintain Ratatui , so I see a lot of the TUIs people build. Many of them end up looking fairly same-ish: plain boxes on a boring background, missing a bunch of the things that good design in other mediums brings us. Borders are part of the problem. They have to take up space, so they become part of the panels people want to build. One option is to drop them and just use color, but borders are really nice for delineating and surrounding things. There are lots of ways to sort of get borders in a terminal, but none that are 100% good. I've drawn enough β”Œβ”€β”€β” by hand to have opinions about this. The usual way is the Unicode Box Drawing block, which has 128 characters for lines, corners and joins. Somehow that's still not enough: So I've been working on a terminal protocol that draws thin lines on the edges of cells instead. I've got it working as a patch to Ghostty (and an earlier version in Rio before that), and I've built demo apps for it with Ratatui. I'd like feedback from terminal and TUI authors. Every line in that screenshot is a cell border. None of them use up a row or column. The tab labels sit on the top edge of each pane, the text fields are underlined right under the text, and the buttons are one row tall and rounded. Here's the same app in a terminal without cell borders, falling back to box-drawing characters: Each pane loses a row and a column on every side, so the log pane shows two fewer lines and the notes wrap differently. The tab labels move inside the frame. The text fields use the underline attribute instead, and the checkbox and buttons turn into brackets. What I'd use it for Borders, obviously. Panes and panels that don't eat a row and column on every side. Menus and popups that sit on top of other content and still look like they're on top. Panel style apps, where the layout is a set of regions that each want an edge. Widgets like text fields, buttons and checkboxes, which want a line right up against their text. Making things look neater in general. A border on the outer edge of a panel's background looks a lot cleaner, and text can run right up to it. Up close The settings form, with cell borders and then with the box-drawing fallback: The tabs are the same story. With cell borders they sit on the pane's top edge. With box characters they need their own row inside the frame: Charts Lines between cells also work for charts. btop-style braille graphs fill every cell with dots, so there's nowhere to put a gridline or an average line without giving up a row. With cell borders they sit on the cell edges between the dots. Here's a monitor in that style, built with Ratatui: And the fallback, which has to drop the gridlines: In motion Here's the tiling panes demo moving panes around, opening menus and the about dialog: Partway through, it turns on a checkerboard of the cells, which shows how everything lines up with the grid. Toggle it here: Show the cell grid What box characters can't do The root of it is that box-drawing characters are text. Each line is a glyph that takes up a whole cell, and pretty much everything annoying about them falls out of that. Space is the obvious one. On my 16" laptop with a font I can actually read, the terminal is something like 200x50 (your prefs will move that around a bit). Columns are mostly fine at that size. Rows aren't. A layout with a couple of levels of nested panels can easily spend 6 or 8 of those 50 rows on nothing but lines. That's a lot of screen to give up for decoration. You also can't put a line between two cells that both have something in them. So there's no way to underline a text field right under its text, or to outline a button, without adding padding around it. Rounded corners sort of work, except the corner glyph has one background color, and when the panel's background is different from what's around it you get a little square of the wrong color in each corner. And the line itself is whatever the font decides it is, weight and position included. Then there's copy and paste. Select some text inside a box and you get the box too. Everyone who's pasted from a TUI into Slack knows the little β”‚ cleanup dance. Sometimes you do want the box (pasting a table into a chat, say), but mostly it's junk. Cell borders aren't text, so they never get copied, which cuts both ways. How it works A cell border is a cell attribute, like a background color. Each cell can carry up to twelve line segments: the two halves of each of its four edges, and four arms from its center. Each segment has its own color, width, placement and style, and the terminal draws them over the cell grid. Borders are stored in the cells, so they scroll, erase, insert and reflow with the text. What happens on erase or scroll already has an answer, the same one as for colors. The edges are split into halves so borders line up with quadrant block characters ( β––β–—β–˜β– and friends). That makes it easy to draw a shape that's a half cell in from the grid and still put a neat border around it. Yes, that still uses a character for the fill, and there might be a better way to do that part. One problem at a time. The same idea could stretch to sextants and octants, or to quarter and eighth widths, if designs need them. Styles and joints Lines can be solid, double, dashed or dotted. Each corner and center of a cell can be square or rounded, with a configurable radius and anti-aliased arcs. A rounded corner can name an outside color, so the area outside the arc is painted correctly when the panel's background differs from what's behind it. When two neighboring cells both put a line on their shared edge, they merge into one line. The stroke with the higher priority wins, then the wider one, then the one to the right or below. That way a focused pane can raise its priority and keep its whole outline, whatever order its neighbors were drawn in. My first try was "right/bottom always wins", which lasted until the focused pane lost half its outline to its neighbor. The wire format Everything is one DCS string with a namespace: ESC P 1 + b cell-border ; OP ; FIELDS... ESC \ The common path is a pen, held in the cursor like the current background color. Set it, print some text, and the printed cells get the borders: ESC P 1 + b cell-border;pen;48;32;7aa2f7;0;0 ESC \ bottom edge, blue: an underline hello ESC [ 0 m SGR 0 clears the pen There are also rectangle commands that set or clear borders on existing cells without touching the pen, and a capability query. An app sends the query followed by DA1. If the DA1 reply arrives without a cell-border reply first, the terminal doesn't support it, so the app falls back to box characters and reserves the space they need. Terminals that don't understand the DCS ignore it, the same as any unknown DCS. The minimum a terminal needs is the pen, the rectangle commands and solid strokes. I've tried to keep that first cut small, because there's always one more "... and this" to add, and that's a slippery slope. Line styles, rounded joints, priority and outside colors are extension fields listed in the capability reply, and unknown fields are ignored, so they can be added later. I'd rather the early decisions be two way doors where possible. The spec calls itself version 1 (it's the 1 in DCS 1 + b ). That's v1 in the old sense, before we all got bogged down in semver. It doesn't mean done or stable, it means it's the first one I was willing to write down. Read it as v0.1, or a draft, or whatever you prefer. Prior art Parts of this have been done before: DEC ruled lines in DECterm

Up nextWhat Happened to 3Com?Technology Β· 6 min read
You're offline β€” showing the last loaded page