feat(config): add program icons and improved title logic to kitty tab bar

This commit is contained in:
Prad Nukala committed 2026-08-31 10:54:38 -04:00
1 parent 745c18852c
commit 703730a4b4
2 files changed
+152 -25

No files matched your search

Binary file not shown.
+152 -25
View File
@@ -1,52 +1,179 @@
# ── tab_bar.py · kitty tab titles: repo dir, or the file nvim is editing ──
# ── tab_bar.py · kitty tab titles: program icon + repo dir (or nvim's file) ──
#
# kitty loads this file from the config dir and exposes `draw_title` as the
# `{custom}` field of tab_title_template (kitty/tab_bar.py:
# load_custom_draw_title). kitty still draws the powerline chrome; only the text
# inside a tab comes from here.
#
# A title is one of:
# 2 chezmoi <- repo the tab's cwd belongs to (dir name if no repo)
# 3 astrocore.lua <- nvim is the active process: the file it is editing
# A title is `<index> <icon> <label>`:
# 2 chezmoi repo the tab's cwd belongs to (dir name if no repo)
# 3 astrocore.lua an editor: the file it is editing
# 4 chezmoi a known program: its icon, repo as the label
#
# No subprocesses, no timers: the repo root is found by walking up for `.git`,
# and the edited file comes from the window title nvim already sets
# (~/.config/nvim/lua/plugins/astrocore.lua, custom_title autocmd:
# "<file> | <org/repo>" or "<file> | Nvim", and "<org/repo>" / "Nvim" with no
# file). kitty repaints the tab bar whenever that title changes, so the tab
# tracks buffer switches for free.
# No subprocesses, no timers. The repo root is found by walking up for `.git`;
# the icon comes from the foreground process' cmdline; the edited file comes from
# the title nvim already sets (~/.config/nvim/lua/plugins/astrocore.lua,
# custom_title autocmd: "<file> | <org/repo>" / "<file> | Nvim", and
# "<org/repo>" / "Nvim" with no file). kitty repaints the tab bar when a window
# title changes, so buffer switches and program changes show up on their own.
from __future__ import annotations
import os
from typing import Any
# Nerd Font glyphs; VictorMono Nerd Font Mono covers both.
FOLDER = '\uf07b' # nf-fa-folder
NVIM = '\ue62b' # nf-custom-neovim
# Nerd Font glyphs; VictorMono Nerd Font Mono covers every codepoint used here
# (checked with `fc-list ":charset=<hex>:family=VictorMono Nerd Font Mono"`).
FOLDER = '\uf07b' # nf-fa-folder — the fallback: "a shell somewhere"
EDITOR = '\ue62b' # nf-custom-neovim
ICONS: dict[str, str] = {
# editors (label becomes the edited file, see EDITORS below)
'nvim': EDITOR,
'vim': EDITOR,
'nvim-qt': EDITOR,
'hx': EDITOR,
'helix': EDITOR,
# agents / harnesses
'claude': '\U000f06a9', # nf-md-robot
'omp': '\U000f09da', # nf-md-brain
'opencode': '\uf121', # nf-fa-code
'crush': '\uf0e7', # nf-fa-bolt
'codex': '\uf120', # nf-fa-terminal
'gemini': '\U000f0674', # nf-md-creation
'aider': '\U000f0068', # nf-md-auto_fix
# git
'lazygit': '\uf1d3', # nf-fa-git
'git': '\uf1d3',
'gitui': '\uf1d3',
'tig': '\uf1d3',
'gh': '\uf113', # nf-fa-github_alt
# containers
'lazydocker': '\uf308', # nf-linux-docker
'docker': '\uf308',
'docker-compose': '\uf308',
'podman': '\uf308',
'kubectl': '\ue7b2', # nf-dev-kubernetes
# multiplexers / terminals-in-terminals
'herdr': '\uf009', # nf-fa-th_large — panes
'tmux': '\uf00a', # nf-fa-th
# files, monitors, history
'yazi': '\uf07c', # nf-fa-folder_open
'ranger': '\uf07c',
'lf': '\uf07c',
'btop': '\uf0e4', # nf-fa-dashboard
'htop': '\uf0e4',
'top': '\uf0e4',
'atuin': '\uf1da', # nf-fa-history
'atuin-pick': '\uf1da',
'ssh': '\uf0ac', # nf-fa-globe
'chezmoi': '\uf013', # nf-fa-cog
# runtimes / build / data
'python': '\ue235', # nf-dev-python
'python3': '\ue235',
'ipython': '\ue235',
'node': '\ue718', # nf-dev-nodejs_small
'bun': '\ue718',
'deno': '\ue718',
'cargo': '\ue7a8', # nf-dev-rust
'rustc': '\ue7a8',
'make': '\uf085', # nf-fa-gears
'just': '\uf085',
'psql': '\uf1c0', # nf-fa-database
'sqlite3': '\uf1c0',
'duckdb': '\uf1c0',
'man': '\uf02d', # nf-fa-book
'less': '\uf02d',
'bat': '\uf02d',
'glow': '\uf02d',
}
# These get the edited file as their label instead of the repo.
EDITORS = frozenset({'nvim', 'vim', 'nvim-qt', 'hx', 'helix'})
# Skipped when reading a cmdline, so `node .../claude` is claude and
# `uv run python x.py` is python — unless nothing else is left, in which case the
# interpreter itself is the program (a bare `python` REPL).
INTERPRETERS = frozenset({
'sh', 'bash', 'zsh', 'fish', 'env', 'sudo', 'doas', 'nohup', 'setsid',
'node', 'bun', 'deno', 'npx', 'uv', 'uvx', 'python', 'python3',
})
# Subcommands of the runners above ("uv run python x.py"), skipped only after a
# runner has been skipped so a program genuinely called `run` still resolves.
RUNNER_WORDS = frozenset({'run', 'exec', 'x', 'tool', '--'})
EDITORS = frozenset({'nvim', 'vim', 'nvim-qt'})
MAX_LEN = 28
# wd -> repo root (or ''), memoized: a working dir keeps its answer for the life
# of the kitty instance, so `git init` in an already-open dir needs a config
# reload (ctrl+shift+alt+, in the herdr host, ctrl+shift+f5 here) to show up.
# reload (ctrl+shift+f5) to show up.
_repo: dict[str, str] = {}
def draw_title(data: dict[str, Any]) -> str:
tab = data['tab']
label = ''
if tab.active_exe in EDITORS:
label = _nvim_file(data['title'])
if label:
return f'{data["index"]} {NVIM} {_elide(label)}'
program = _program(_cmdline(data['tab_id']), tab.active_exe)
index = data['index']
if program in EDITORS:
edited = _nvim_file(data['title'])
if edited:
return f'{index} {ICONS[program]} {_elide(edited)}'
icon = ICONS.get(program, FOLDER)
wd = tab.active_wd
label = ''
if wd:
label = os.path.basename(_repo_root(wd) or wd.rstrip('/'))
if wd == os.path.expanduser('~'):
label = '~'
return f'{data["index"]} {FOLDER} {_elide(label or data["title"])}'
label = '~' if wd == os.path.expanduser('~') else os.path.basename(_repo_root(wd) or wd.rstrip('/'))
return f'{index} {icon} {_elide(label or data["title"])}'
def _cmdline(tab_id: int) -> list[str]:
"""argv of the tab's active window's foreground process, [] if unavailable."""
from kitty.boss import get_boss
try:
tab = get_boss().tab_for_id(tab_id)
window = tab.active_window if tab is not None else None
return list(window.child.foreground_cmdline) if window is not None else []
except Exception:
return []
def _program(cmdline: list[str], fallback_exe: str) -> str:
"""Program name a tab should be iconified by.
Walks the cmdline past runners and flags: `node …/claude` is claude, an
unknown script keeps its runtime's icon (`uv run python train.py` is
python, `bun run dev` is bun), and a bare `python` stays python. Falls back
to kitty's own exe for the active window when the cmdline is unreadable.
"""
runtime = ''
skipped = False
for arg in cmdline:
if arg.startswith('-'):
continue
name = os.path.basename(arg).lower()
if name.endswith(('.js', '.mjs', '.cjs', '.py', '.ts')):
name = name.rsplit('.', 1)[0]
if not name:
continue
name = _canonical(name)
if name in INTERPRETERS or (skipped and name in RUNNER_WORDS):
skipped = True
runtime = runtime or (name if name in ICONS else '')
continue
return name if name in ICONS or not runtime else runtime
return runtime or _canonical(os.path.basename(fallback_exe or '').lower())
def _canonical(name: str) -> str:
"""`omp-linux-x64` -> `omp`, `docker-compose` stays itself."""
if name in ICONS:
return name
for known in ICONS:
if name.startswith(known + '-'):
return known
return name
def _repo_root(wd: str) -> str:
@@ -67,7 +194,7 @@ def _repo_root(wd: str) -> str:
def _nvim_file(title: str) -> str:
"""The file name out of an nvim window title, '' when it is editing none.
"""The file name out of an editor window title, '' when it is editing none.
Handles this config's "<file> | <org/repo>" / "<file> | Nvim" and nvim's own
default "%t - Nvim" / "<file> (<dir>) - NVIM".