Kitmux
A native Mac terminal for people who keep a lot of work running at once.
Kitmux is a work in progress. Version 0.2.0 is an early preview for Apple silicon Macs, not notarized yet, so macOS asks before the first launch. An experimental Linux port is also underway.
How libkitty was built
We didn't rewrite Kitty. We found the parts that could live inside another app, tested them one at a time, and wrapped the working pieces in a small C library. The first goal was modest: get the word hello through Kitty's parser from a C program. A native Mac window came later.
-
We took Kitty apart on paper first
Before touching the code, we traced where everything lived. Kitty's
screen.c, line buffer, history, resize code, and VT parser handled the terminal itself. GLFW handled windows and the main event loop. Other pieces, especially input and shaders, reached into Kitty's global window state. That was the messy bit. Kitty's own test suite gave us the opening we needed because it creates aScreenwith no window at all. We kept the screen, parser, configuration, fonts, and renderer. The window system would belong to Kitmux. -
We locked it to one known Kitty version
Kitty moves quickly, so libkitty started from one pinned source commit. The build uses the same CPython installation that compiled Kitty's
fast_data_typesextension. That detail matters. Kitty's C engine is built as a Python extension, which means Python is part of the machinery even though the finished app doesn't look or feel like a Python app. One script builds Kitty, applies the small render patch, and runs Kitty's screen tests. If an upstream change breaks the patch later, the build fails loudly. -
Then came the smallest possible test
A tiny C program started Python inside itself, imported Kitty, created a five-line
Screen, and fed ithello from libkitty. Then it read the cells back. No shell yet. No window either. Seeing those words come back proved that Kitty could run inside a program that Kitty didn't own. There was no hidden terminal process and no messages bouncing between two apps. The C host was touching the live Kitty screen in its own memory. -
We gave the screen a real shell
The next test opened a pseudo-terminal, usually called a PTY, and launched a shell inside it. A PTY is the pipe that makes a command-line program believe it is talking to a terminal. Shell output travels into Kitty's parser. Keyboard input goes the other way. Kitty sometimes sends answers back to the shell, such as a cursor-position report, so that return path had to work too. Resizing changes both Kitty's grid and the PTY's row and column count. This became the basic session loop libkitty still uses: create, pump, write, resize, and close.
-
The Mac app stayed in charge
Normal Kitty expects GLFW to run the whole application's event loop. Kitmux couldn't hand over that control because AppKit needs to own the main thread, windows, timers, and mouse and keyboard events. libkitty uses a pull model instead. Kitmux watches each PTY and calls
kitty_session_pumpwhen output is ready. The call doesn't block. Any libkitty function that touches Python also takes the Python GIL for the few moments it needs it. Python sits quietly inside the engine, and the Mac app keeps running the show. -
Getting Kitty's renderer out took one small patch
We tested drawing in an invisible OpenGL buffer before trying a real window. Kitty hides most of its render functions inside
fast_data_types, so the host couldn't call them directly. The project adds one patch of roughly 180 lines inside Kitty. It exposes just enough to start OpenGL without GLFW, load fonts, create the small slice of window state the shaders expect, upload screen cells, and call Kitty's owndraw_cellsfunction. The first successful image contained real red terminal text. It came from Kitty's font shaping, glyph atlas, colors, cursor, selection marks, and shaders. We didn't build a lookalike renderer. -
The experiments became a small C library
Once the spikes worked, their useful parts moved into five places:
libkitty.h,engine.c,session.c,render.c, and a thin Python glue file. One shared engine owns Python and the renderer. Every terminal session gets its own KittyScreen, child process, PTY, scrollback, selection, search state, and callbacks. The host receives plain C callbacks when the screen changes, a title updates, a bell rings, a notification arrives, the working directory changes, or a child exits. Swift never has to manage a Python object or know the shape of Kitty's internal structs. -
New features kept using the same boundary
After the first window worked, libkitty grew a call at a time. Bracketed paste came first. Scrollback, selection, search, Kitty keyboard encoding, mouse reporting, font-size changes, pane geometry, notifications, working-directory tracking, and safe scrollback replay followed. Each session still looks like the same object to its host. C and Python tests cover startup, PTY traffic, separate sessions, input encoding, size limits, rendering, and failures. A 140-line AppKit example runs a real shell in a real window. Kitty parses and draws the terminal. libkitty makes that engine usable. Kitmux decides what kind of app to build around it.
Where it stands now: libkitty runs on macOS, uses CPython 3.14 or newer, and carries the same GPLv3 license as Kitty. The project maintains one small Kitty patch instead of carrying a second terminal emulator.
How Kitmux was built
Kitmux started with one AppKit window and one shell. That was enough to prove Swift could create a libkitty session, send it keystrokes, and draw it inside an NSOpenGLView. Splits came next. Each pane received its own shell and Kitty Screen, while a small tree kept track of how much space every pane should get.
Tabs, groups, and named workspaces were layered onto that same model. A workspace owns its groups. A group owns tabs. A tab owns its split tree and live panes. This hierarchy gives every click and keyboard shortcut an exact destination. It also lets Kitmux save the layout and rebuild it with fresh shells after a restart.
The daily-use pieces arrived one by one: paste, scrollback, selection, search, mouse support inside apps like vim and tmux, draggable dividers, notifications, a command palette, settings, and a local kitmux command. Most behavior has a small model test. Terminal behavior is tested again through libkitty, and packaged-app smoke checks make sure the pieces still work together in a Mac window.
Swift owns the app's personality and structure. It draws the sidebar and tab strip, remembers the workspace layout, routes input, and decides which pane has focus. libkitty stays focused on terminal work. That separation is why Kitmux can keep changing its interface without having to rebuild Kitty's parser or renderer.