kitty_panels
kitty_panels
A Crystal shard that turns kitty terminal windows into desktop panels: dropdowns, docks and overlays, each running its own command with its own environment and per-panel kitty settings. Everything is driven over kitty's remote control protocol, so a stock kitty build is all it needs.
Requirements
- Crystal >= 1.21
- kitty >= 0.34 (the remote control protocol version this shard speaks)
Installation
dependencies:
kitty_panels:
path: /path/to/kitty_panels
require "kitty_panels"
Usage
require "kitty_panels"
definitions = KittyPanels.load_definitions("panels.yaml")
engine = KittyPanels::Engine.new("/tmp/kitty-panels.sock")
engine.start
engine.wait_ready
rc = KittyPanels::RC.new("/tmp/kitty-panels.sock")
definitions.each { |panel| rc.launch_panel(panel) }
# ...
engine.terminate
KittyPanels::RC can also drive an already running kitty that listens on a socket (kitty --listen-on unix:/path/to/socket). Pass a password to use kitty's encrypted protocol (KITTY_PUBLIC_KEY must come from the kitty instance that owns the socket).
Configuration
panels: maps panel names to definitions. Panel names may use letters, digits, - and _. Every panel needs a cmd (argv array):
panels:
dropdown:
mode: dropdown
edge: top
lines: "40"
layer: overlay
focus_policy: exclusive
hide_on_focus_loss: true
kitty_overrides:
background_opacity: "0.85"
cmd: [/bin/bash, -l]
statusbar:
mode: dock
edge: bottom
lines: "2"
focus_policy: not-allowed
exclusive_zone: -1
cmd: [/bin/sh, -c, "while true; do date; sleep 5; done"]
See examples/panels.yaml for the shipped example.
| key | default | notes |
|---|---|---|
cmd |
— required | argv of the command that runs inside the panel |
mode |
dropdown |
dropdown, dock, overlay; cannot change on a live panel |
edge |
top |
top, bottom, left, right, center, center-sized |
lines |
25 |
height; cells, 200px or 10c |
columns |
unset | width; same units as lines |
layer |
top |
background, bottom, top, overlay |
focus_policy |
on-demand |
not-allowed, exclusive, on-demand |
margin_top/bottom/left/right |
0 |
integer |
output_name |
unset | restrict the panel to one monitor |
exclusive_zone |
unset | integer, e.g. -1 |
override_exclusive_zone |
false |
boolean |
hide_on_focus_loss |
false |
boolean |
app_id |
kitty-panels |
OS window class |
env |
{} |
KEY: VALUE mapping exported into the panel |
kitty_overrides |
{} |
per-panel kitty options, see below |
kitty_overrides
Only options kitty can apply to an individual window are accepted; anything else is rejected at load time. Supported keys:
background_opacity— a number between 0 and 1- any kitty color option (
foreground,background,cursor,selection_*, tab bar and mark colors, border colors, titlebar colors) pluscolorNforN<= 255 — values are#rgb,#rrggbb, an X11 color name ornone window_padding_width,window_margin_width— numbersenabled_layouts— comma-separated layout namesligatures—never,alwaysorcursorbackground_image— image path ornone;background_image_layout—configured,tiled,mirror-tiled,scaled,clamped,centeredorcscaled(requiresbackground_image)window_logo— image path ornone;window_logo_positionandwindow_logo_alpha(0 to 1) requirewindow_logo
Overrides are applied right after the panel launches. They cannot be changed on a live panel — recreate the panel instead.
Live reconfiguration
A subset of settings can be changed on a live panel with PanelDefinition#with_settings(settings), which returns a validated copy without touching the original: edge, layer, lines, columns, margin_*, focus_policy, output_name, exclusive_zone, override_exclusive_zone and hide_on_focus_loss. Flags can only be switched on and settings cannot be cleared — recreate the panel for that. Send the result to kitty with rc.configure_panel(name, args).
API
KittyPanels.load_definitions(path) : Array(PanelDefinition)— YAML loader; validation errors name the file, panel and line.KittyPanels::Engine.new(socket_path, kitty = "kitty", config_path = nil, log_path = nil)— spawns a dedicated hidden kitty listening on a UNIX socket with socket-only remote control. Config and log default to<socket_path>.confand<socket_path>.log. Methods:start,wait_ready(timeout),exited?,wait_exit,terminate(grace).KittyPanels::RC.new(socket_path, password = nil)— remote control client:alive?,ls,live_panels : Hash(String, LivePanel),first_live_panel,launch_panel(defn, hidden = false) : Int64,configure_panel(name, os_panel_args),send_text(name, text),close_panel(name),panel_match(name). A failed launch rolls back.KittyPanels::PanelDefinition—apply(key, value)(string-based, for CLI front ends),with_settings(settings),to_os_panel_args(only),validate!,clone; JSON/YAML serializable using wire spellings such ascenter-sizedornot-allowed.
Every panel runs with KITTY_PANELS_PANEL_NAME in its environment and the user var kitty_panels_name, which panel_match turns into kitty's var:kitty_panels_name=<name> matching expression.
Panel manager
The shard ships a kitty_panels executable that runs a panel manager daemon: it starts a dedicated hidden kitty, launches every panel from the config, arranges tiled panels from a panel tree (columns, rows, grid, master-stack), keeps docks as screen-edge struts, and reconciles geometry against KWin on KDE Plasma Wayland.
kitty_panels daemon --config panels.yaml
kitty_panels ctl tree
kitty_panels ctl resize left 0.5
The same YAML file can carry a manager: section:
manager:
layout: master-stack # columns, rows, grid or master-stack
master_ratio: 0.6
grid_columns: 3 # used by grid
gap: 8 # pixels between tiled panels
min_size: 200 # minimum tiled panel size
panels: [a, b] # optional; defaults to every non-dock panel
socket: /tmp/kitty_panels/ctl.sock
Commands: tree, launch NAME, close NAME, toggle NAME, focus NAME|next|prev, split EXISTING NEW h|v, move NAME h|v TARGET, resize NAME RATIO, layout PRESET, reconcile. Docked panels (mode: dock) keep their kitty edge settings and reserve screen space; tiled panels launch with a neutral edge and are placed by the manager in pixels, inside KWin's maximize area — so external struts like the Plasma panel are respected. output_name pins a panel to a specific monitor (default: the primary output); each output gets its own tree and focus cycles across outputs in order.
The daemon adopts an already-running kitty engine when one listens on --engine-socket, and persists window identities in <socket>.state.json, so a crash leaves the panels running and a restart re-adopts them without relaunching. focus activates the window through KWin as well as kitty. The KWin backend needs qdbus and dbus-monitor on PATH.
Testing
crystal spec # or: make test
RC-level specs run against an in-process FakeKitty that speaks the remote control protocol, and manager specs against Positioner::Fake, so no real kitty is needed.
License
MIT
kitty_panels
- 0
- 0
- 0
- 0
- 0
- about 2 hours ago
- October 3, 2026
Sat, 17 Oct 2026 17:14:41 GMT