<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nick Skriabin</title>
    <description>The latest articles on DEV Community by Nick Skriabin (@nick_skriabin).</description>
    <link>https://dev.to/nick_skriabin</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1385863%2F304383a4-1fd9-4d75-941c-439c6c254077.jpeg</url>
      <title>DEV Community: Nick Skriabin</title>
      <link>https://dev.to/nick_skriabin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nick_skriabin"/>
    <language>en</language>
    <item>
      <title>Attyx: tiny and fast GPU accelerated terminal emulator</title>
      <dc:creator>Nick Skriabin</dc:creator>
      <pubDate>Sat, 28 Feb 2026 19:37:28 +0000</pubDate>
      <link>https://dev.to/nick_skriabin/attyx-tiny-and-fast-gpu-accelerated-terminal-emulator-1fbd</link>
      <guid>https://dev.to/nick_skriabin/attyx-tiny-and-fast-gpu-accelerated-terminal-emulator-1fbd</guid>
      <description>&lt;p&gt;I live in my terminal. Neovim, tmux, git, SSH — that's my whole day. I've used every terminal emulator out there. iTerm2, Alacritty, Kitty, Ghostty. All great.&lt;/p&gt;

&lt;p&gt;But I never understood what actually happens inside one. Bytes come out of a shell, escape sequences get parsed, characters appear on screen. What does &lt;code&gt;ESC[38;2;255;100;0m&lt;/code&gt; &lt;em&gt;do&lt;/em&gt; to the internal state? How does a key press travel through a pseudoterminal and come back as text? I had no clue.&lt;/p&gt;

&lt;p&gt;Only way I know how to learn something is to build it.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://github.com/semos-labs/attyx" rel="noopener noreferrer"&gt;Attyx&lt;/a&gt; — a GPU-accelerated terminal emulator, from scratch, in Zig. Started on a Saturday. Five days later I was daily-driving it. I'm still daily-driving it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Though
&lt;/h2&gt;

&lt;p&gt;"We don't need another terminal emulator." Sure. But I didn't build it because the world needed one. I built it for two selfish reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I wanted to learn Zig.&lt;/strong&gt; Not from docs and tutorials — from a real project that would punch me in the face with systems programming problems. Terminal emulators hit everything: parsing, GPU rendering, font rasterization, Unicode, PTY management, platform APIs. Perfect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I needed a testable terminal core.&lt;/strong&gt; This one's less obvious, so let me explain.&lt;/p&gt;

&lt;p&gt;I build TUI apps. I made &lt;a href="https://semos.sh/glyph/" rel="noopener noreferrer"&gt;Glyph&lt;/a&gt; — a React renderer for the terminal. Flexbox, components, hooks, the whole deal, but rendering to your terminal instead of a browser. On top of it I built &lt;a href="https://semos.sh/aion/" rel="noopener noreferrer"&gt;Aion&lt;/a&gt; (Calendar TUI) and &lt;a href="https://semos.sh/epist/" rel="noopener noreferrer"&gt;Epist&lt;/a&gt; (an email client with vim keybindings). Real apps I use every single day.&lt;/p&gt;

&lt;p&gt;Here's my problem: how do you test what a TUI app actually &lt;em&gt;looks like&lt;/em&gt;? You can test component state. You can test logic. But the final output — the grid of characters and styles after the terminal interprets your escape sequences — that's a black hole. Screenshot diffing? Fragile. Asciinema recordings? Not automated. Unit testing raw escape codes? Doesn't catch interpretation bugs.&lt;/p&gt;

&lt;p&gt;What I wanted was dead simple — feed bytes into a terminal engine, get a deterministic grid back, assert against it. No terminal app existed that let me do that. So I built one where the core is pure from the ground up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight zig"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;Engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;allocator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;my_app_output&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;cell&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="py"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="py"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;expectEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'A'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="py"&gt;char&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="py"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="py"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No GPU. No window. No PTY. Just bytes in, state out. That's the testing primitive I've wanted for years.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Get
&lt;/h2&gt;

&lt;p&gt;GPU-accelerated rendering — Metal on macOS, OpenGL 3.3 on Linux. Full VT100/xterm compatibility. Truecolor, 256-color, the works. Mouse tracking, Kitty graphics protocol for inline images, hyperlinks, alternate screen buffer, scroll regions, cursor shapes. 20,000-line scrollback with reflow on resize. Popup terminals that float over your session. Search. TOML config with hot reload.&lt;/p&gt;

&lt;p&gt;Under 5MB.&lt;/p&gt;

&lt;p&gt;The whole thing — GPU rendering, VT parser, font handling, platform code — in a tiny binary. That's what happens when you write Zig with minimal deps, use native platform frameworks instead of bundling a GUI toolkit, and let the compiler strip dead code. No runtime. No GC. No Electron. Just a Zig binary talking to the OS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the Hood (Without Boring You to Death)
&lt;/h2&gt;

&lt;p&gt;I wrote a &lt;a href="https://semos.sh/blog/building-attyx/" rel="noopener noreferrer"&gt;deep technical dive&lt;/a&gt; on the Semos blog if you want all the details. But here's the gist.&lt;/p&gt;

&lt;p&gt;Everything flows through a pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Raw bytes → Parser → Actions → State → Grid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The parser&lt;/strong&gt; is an incremental state machine. One byte at a time, spits out actions — "print H", "move cursor to row 3 col 5", "set foreground to red." Fixed-size buffers, zero heap allocations, handles partial sequences across &lt;code&gt;read()&lt;/code&gt; boundaries. The whole parser struct is stack-sized.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The grid&lt;/strong&gt; is a flat array of cells. Each cell: a Unicode codepoint, two combining mark slots (for diacriticals and such), a style, and a hyperlink ID. One alloc on init. Scroll regions are &lt;code&gt;memcpy&lt;/code&gt; on contiguous memory. No linked lists, no indirection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Damage tracking&lt;/strong&gt; keeps rendering fast. 256-bit dirty bitset — four &lt;code&gt;u64&lt;/code&gt;s. State machine flips a bit when it touches a row. Renderer only redraws dirty rows. Most frames, that's one or two rows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two threads, no locks.&lt;/strong&gt; PTY thread reads bytes and fills a shared cell buffer. Main thread renders at vsync. A seqlock — just an atomic generation counter — prevents torn reads. If the renderer catches the PTY mid-write, it skips a frame. You'll never notice a single dropped frame. You will notice mutex contention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GPU rendering&lt;/strong&gt; turns each character into a textured quad. Glyphs get rasterized on demand into an atlas that lives in GPU memory. Drawing 10,000 cells costs about the same as drawing 100 — that's the whole point of offloading to the GPU. The CPU does parsing and state. The GPU does pixels. Each does what it's good at.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Semos Stack
&lt;/h2&gt;

&lt;p&gt;Attyx is part of &lt;a href="https://semos.sh" rel="noopener noreferrer"&gt;Semos&lt;/a&gt; — the collection of dev tools I've been building. It fits into a stack that's been growing over the past few months:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://semos.sh/glyph/" rel="noopener noreferrer"&gt;Glyph&lt;/a&gt;&lt;/strong&gt; is the React terminal renderer. Write terminal apps with JSX, flexbox, hooks — the same DX you know from the web, but painting to a terminal. &lt;strong&gt;&lt;a href="https://semos.sh/aion/" rel="noopener noreferrer"&gt;Aion&lt;/a&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;a href="https://semos.sh/epist/" rel="noopener noreferrer"&gt;Epist&lt;/a&gt;&lt;/strong&gt; are the apps built on top of it — calendar and email, both living in my terminal where they belong.&lt;/p&gt;

&lt;p&gt;Attyx closes the loop. Glyph apps needed a testable terminal. Attyx needed real-world apps to stress-test against. They push each other forward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give It a Shot
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;semos-labs/tap/attyx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or build from source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/semos-labs/attyx
&lt;span class="nb"&gt;cd &lt;/span&gt;attyx
zig build &lt;span class="nt"&gt;-Doptimize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ReleaseFast
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or &lt;a href="https://semos.sh/attyx" rel="noopener noreferrer"&gt;download it&lt;/a&gt; from the website if you happened to be on a mac. You will also get auto-updates in this case.&lt;/p&gt;

&lt;p&gt;Config goes in &lt;code&gt;~/.config/attyx/attyx.toml&lt;/code&gt; — fonts, colors, keybindings, opacity, blur. Change it, hit &lt;code&gt;Ctrl+Shift+R&lt;/code&gt;, done. No restart.&lt;/p&gt;

&lt;p&gt;Open source, MIT licensed. &lt;a href="https://github.com/semos-labs/attyx" rel="noopener noreferrer"&gt;github.com/semos-labs/attyx&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Is it as mature as Ghostty or Kitty? Not yet. But I understand every line in it, I use it every day, and the whole thing fits in under 5MB. That counts for something.&lt;/p&gt;

</description>
      <category>cli</category>
      <category>terminal</category>
      <category>tui</category>
      <category>gpu</category>
    </item>
  </channel>
</rss>
