kitmux

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.

  1. 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 a Screen with no window at all. We kept the screen, parser, configuration, fonts, and renderer. The window system would belong to Kitmux.

  2. 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_types extension. 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.

  3. Then came the smallest possible test

    A tiny C program started Python inside itself, imported Kitty, created a five-line Screen, and fed it hello 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.

  4. 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.

  5. 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_pump when 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.

  6. 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 own draw_cells function. 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.

  7. 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 Kitty Screen, 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.

  8. 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.