Open source
A dev-first terminal

A terminal, built to the pixel.

Native panes, hideable drawers, tool floats, and workspaces you define, all on a libghostty core. Reach your toolchain without stopping to find somewhere to run it.

macOS 14+, Apple Silicon.

14 import AppKit
13
12 /// One pane's focus ring. The window owns the colour.
11 final class FocusRing: CALayer {
10 private var pane: PaneID?
9
8 func move(to frame: CGRect) {
7 frame = frame
6 borderColor = Theme.current.chrome.accent.cgColor
5 }
4
3 func follow(_ pane: Pane) {
2 guard pane.id != self.pane else { return }
1 move(to: pane.frame)
41 self.pane = pane.id
1 }
2 }
3
4 extension FocusRing {
5 static let width: CGFloat = 1
6 }
NORMALzen-termSources/ZenTerm/FocusRing.swiftswiftmain
Bottom drawer⌘B
$ pnpm dev
▲ Next.js 16.1.6 (Turbopack)
- Local: http://localhost:3000
- Network: http://192.168.1.14:3000
✓ Ready in 562ms
✓ Compiled /docs/workspaces in 240ms
Right drawer⌘\
The ring reads its colour from the theme rather than the window, so a theme switch repaints it with everything else.
● Bash(swift build)
… +6 lines
● Build succeeded. Running swift test.
● 214 tests passed.
✻ Verifying the reconciler… (2m 14s)
❯ split the reconciler test
zen-termmain ⇡9 ~1auto mode on
zen-term
1 editor
2 tests
3 logs

one workspace: editor, dev server, agent

Keyboard-first

Every action is a keystroke: palettes, panes, drawers, floats. Your hands stay on home row.

Own the UI

Catppuccin, Tokyo Night, Nord and Gruvbox ship built in, or point ZenTerm at any ghostty theme. The whole app follows: terminal, chrome, tabs, and the focus halo.

Define your workflow

Build your workspaces, define your tools, configure the terminal defaults, and remap your shortcuts.

Workspaces
Agents
Wants to run swift testzen-498-toast-queue · claude
Rewriting the pane testszen-term-website · claude
Editing QueueView.swiftzen-review · codex
Docs build, 3 of 9 pageszen-term-website · claude
Release notes page buildszen-term-website · claude
1 import AppKit​ 2 ​ 3 final class SidebarView: NSView {​ 4 private let workspaces: WorkspaceList​ 5 private let agents: AgentRoster​ 6 private let worktrees: WorktreeList​ 7 private var isCollapsed = false​ 8 ​ 9 static let expandedWidth: CGFloat = 240​ 10 static let railWidth: CGFloat = 72​ 11 ​ 12 func render(_ state: SidebarState) {​ 13 workspaceSection.render(state.workspaces)​ 14 agentSection.render(state.agents)​ 15 worktreeSection.render(state.hosts)​ 16 }​ 17 ​ 18 func jump(to agent: AgentID) {​ 19 guard let target = agents.locate(agent) else { return }​ 20 onSelectWorkspace(target.workspace)​ 21 onSelectTab(target.tab)​ 22 onFocusPane(target.pane)​ 23 }​ 24 }​ 25 ​ 26 ​ 27 ​ 28 ​ 29 ​ 30 ​ 31 ​ 32 ​ 33 ​ 34 ​ 35 ​ 36 ​ 37 ​
NORMALSidebarView.swiftswift19:9
~/Dev/zen-512-sidebar zen-512-sidebar​ ❯ swift build​ Building for debugging...​ [412/418] Compiling ZenTerm SidebarView.swift​ [417/418] Linking ZenTerm​ Build complete! (8.42s)​ ​ ~/Dev/zen-512-sidebar zen-512-sidebar​ ❯
~/Dev/zen-512-sidebar zen-512-sidebar​ ❯ git status -s​ M Sources/ZenTerm/WindowController.swift​ M Sources/ZenTerm/TabBarView.swift​ ?? Sources/ZenTerm/SidebarView.swift​ ?? Sources/ZenTerm/AgentRoster.swift​ ​ ~/Dev/zen-512-sidebar zen-512-sidebar​ ❯
1 editor
2 tests
3 logs
Workspaces

Pin a workspace to any folder.

A workspace pins panes, processes, and environment to a folder. Set it up once and the project opens that way every time you launch it.

  • Panes, processes, and environment, per workspace
  • A window holds several, listed in the sidebar
  • Curated by hand, never a directory scan
12 import AppKit 13 14 final class PaneFocusRing: CALayer, PaneDecoration { // one ring per grid, reused across splits and closes, so a closed pane never leaves a lit border behind 15 let width: CGFloat = 1 16 let radius: CGFloat = 12 17 18 func path(in rect: CGRect) -> NSBezierPath { 19 let inset = rect.insetBy(dx: width / 2, dy: width / 2) 20 return NSBezierPath(roundedRect: inset, xRadius: radius, yRadius: radius) // inset keeps the stroke inside the pane bounds 21 } 22 23 func focusDidChange(_ pane: Pane) { // the grid calls this, the ring never asks. Focus lives in one place and the ring draws what it is handed 24 guard pane.isFocused else { isHidden = true; return } 25 isHidden = false 26 setNeedsDisplay() 27 } 28 } 29 30 extension PaneGrid { // Sources/ZenTerm/Panes/PaneGrid+Focus.swift holds the movement, the grid holds the state 31 func focus(_ direction: Direction) { 32 guard let next = neighbor(of: focused, in: direction) else { return } 33 focused = next 34 ring.focusDidChange(next) 35 } 36 37 func split(_ axis: Axis) { 38 let pane = Pane(profile: focused.profile) 39 insert(pane, after: focused, along: axis) // ⌘D splits down the vertical axis 40 focused = pane 41 } 42 } 43 44 struct FocusMode { 45 var isOn: Bool = false 46 var dimmed: CGFloat { isOn ? 0.35 : 1 } // unfocused panes fade, the focused pane holds its color so the eye lands on one place 47 } 48 49 // The grid owns focus, the ring only draws it. A split that closes hands focus back before the ring redraws. 50 extension PaneGrid: FocusReceiver { 51 var focusedPane: Pane { focused } 52 } 53
[1] Status
zen-term → feature/pane-focus-ring
[2] Files - Worktrees - Submodules
▼ ▪ Sources/ZenTerm
▼ ▪ Panes
M ▪ PaneFocusRing.swift +24 -6
M ▪ PaneGrid.swift +9 -3
?? ▪ FocusMode.swift
▼ ▪ Tabs
M ▪ TabBarView.swift +12
▼ ▪ Settings
M ▪ AppearanceSection.swift +5 -5
▼ ▪ Tests/ZenTermTests
?? ▪ PaneFocusTests.swift
1 of 9
[3] Local branches - Remotes - Tags
* feature/pane-focus-ring
1d main ✓
2d feature/tool-float-sizing ✓ ↓3
4d feature/settings-appearance ✓
1 of 4
[4] Commits - Reflog
c6e1e36 ● ○ Draw the focus ring inside the pane
e9ea16d ● ○ Keep the ring visible while a pane resi
a8b1197 ● ○ Move pane focus state onto the grid
6b1eb59 ● ○ Restore focus after a split closes
1902c54 ● ○ Add PaneFocusTests for the ring path
e43f5b0 ● ○ Trim the ring radius to match the pane
1 of 67
[5] Stash 0 of 0
[0] Unstaged changes
Δ Sources/ZenTerm/Panes/PaneFocusRing.swift
 
• func path(in rect: CGRect) -> NSBezierPath {
// Inset by half the stroke so the ring stays
// inside the pane it belongs to.
let inset = rect.insetBy(dx: width / 2, dy: width / 2)
- return NSBezierPath(roundedRect: rect, xRadius: radius)
+ return NSBezierPath(roundedRect: inset, xRadius: radius)
}
 
• func focusDidChange(_ pane: Pane) {
guard pane.isFocused else { isHidden = true; return }
isHidden = false
setNeedsDisplay()
}
 
// The grid owns focus. The ring reads it and draws,
// so a split never leaves two rings lit at once.
func focus(_ direction: Direction) {
guard let next = neighbor(of: focused, in: direction)
else { return }
focused = next
ring.focusDidChange(next)
}
 
// Focus mode dims every pane the ring is not on.
func dim(_ pane: Pane) -> CGFloat {
pane.id == focused.id ? 1 : FocusMode.dimmed
}
1 of 4
Stage: <space> | Commit: c | Stash: s | Discard: d | Reset: D | Keybindings: ?
Donate Ask Question 0.62.2
1 editor
2 build
3 git
Floats and drawers

Open a tool over your work, then hide it.

Drawers stay hidden until you call them. Open a tool from any workspace, then kill it or leave it running in the background, out of view.

  • Scope a float to a window, a directory, or a tab
  • Any TUI works: lazygit, btop, yazi, ghdash
  • All of it, configurable
~/Dev/zen-term feature/pane-focus-ring ❯ swift build [412/418] Compiling ZenTerm PaneFocusRing.swift [417/418] Linking ZenTerm Build complete! (7.91s) ~/Dev/zen-term feature/pane-focus-ring ❯ swift test --filter PaneFocusTests Test Suite 'PaneFocusTests' started Executed 18 tests, 0 failures (0.42s) ~/Dev/zen-term feature/pane-focus-ring ❯
1 import AppKit 2 3 struct PaneFocusRing { 4 let width: CGFloat = 1 5 let radius: CGFloat = 12 6 7 func path(in rect: CGRect) -> NSBezierPath { 8 NSBezierPath(roundedRect: rect, 9 xRadius: radius, 10 yRadius: radius) 11 } 12 } 13
~/Dev/zen-term feature/pane-focus-ring ❯ git status -s M Sources/ZenTerm/Panes/PaneFocusRing.swift M Sources/ZenTerm/Panes/PaneGrid.swift ?? Sources/ZenTerm/Panes/FocusMode.swift ~/Dev/zen-term feature/pane-focus-ring ❯
Search commands…
Workspaces
Open Workspace Picker⌘ P
New Workspace⌘ ⌃ T
New Worktree…⌥ ⏎
Close Workspace⌘ ⌃ W
Tabs
New Tab⌘ T
Move Tab Left⌘ ⌥ [
Window
Toggle Sidebar⌘ ⌃ S
Fill Screen⌘ ⏎
Panes
Split Vertically⌘ D
Focus Mode⌘ ⇧ ⏎
⏎run ↑↓move ⎋close
1 editor
2 build
3 git
Palette and config

Make it yours, find it fast.

Change the behavior, appearance, and feel. Every action lives in the command palette for instant access.

  • Floats, workspaces, theme, keybinds, terminal behavior
  • In the app, or in the config file
  • Discovery behind a unified command picker
Neovim

One motion across Neovim splits and ZenTerm panes.

Your Neovim splits and ZenTerm panes become one surface to move through. Bind the same motion shortcuts on both, and zen-navigator.nvim carries you across the seam: reach the edge of your splits and you land in the pane beside them, then back. It's vim-tmux-navigator, without the tmux.

View zen-navigator.nvim
1. Install with lazy.nvim
{ "praxis-labs-io/zen-navigator.nvim", event = "VeryLazy", opts = {} }
2. Hand ZenTerm the chord in your config
keybind = nav_left=ctrl+h
keybind = nav_down=ctrl+j
keybind = nav_up=ctrl+k
keybind = nav_right=ctrl+l
Form meets function

Reach any of it without leaving home row.

An interface built around pixel-perfect functionality.

01

Floating-pane canvas

Terminals sit inset on a padded canvas, rounded frames with room to breathe around each pane.

02

The focus halo

The focused pane carries an accent ring and soft glow, so you can see where the keyboard is pointed.

03

Panes that keep their shells

Split a pane in any direction. Restructure the layout and your running processes stay put.

04

Focus Mode

It fills the window with the focused pane, then restores your layout untouched.

05

Bring your own theme

Aim ZenTerm at any ghostty theme and one portable file colors the entire app: terminal, chrome, tabs, halo.

06

Know when an agent needs you

When ZenTerm sits in the background and an agent stops for input, a system notification pulls you back.

How it compares

Opinionated where it counts.

No knock on the classics. ZenTerm makes different bets, and this is where they show up.

ZenTermiTerm2WarpGhosttyKittyTerminal.app
Floating-pane canvas + focus halo
Hideable drawers, alive when hidden
Summonable tool floats (per-tool, any TUI)PartialPartial
Workspace recipes (layout + processes)PartialPartial
Themes a non-cell UI, not just the gridPartial
In-app configuration
Keyboard-first command palettePartial
No account, no cloudPartial
Open source

Get ZenTerm.

The app is public under the MIT license. Read how any of it works, build it yourself, or open a pull request. Each release has notes on what changed.