From e76f070caa160a831fc9cb68fc90ec75dab8fdf2 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Mon, 31 Aug 2026 10:48:53 -0400 Subject: [PATCH] feat(config): display repo name or active nvim file in kitty tab titles --- .../__pycache__/pass_keys.cpython-314.pyc | Bin 3156 -> 0 bytes dot_config/kitty/kitty.conf | 6 +- dot_config/kitty/tab_bar.py | 258 +++++------------- 3 files changed, 69 insertions(+), 195 deletions(-) delete mode 100644 dot_config/kitty/__pycache__/pass_keys.cpython-314.pyc diff --git a/dot_config/kitty/__pycache__/pass_keys.cpython-314.pyc b/dot_config/kitty/__pycache__/pass_keys.cpython-314.pyc deleted file mode 100644 index d5cc738bee37684af4c9e6e2111967a894137415..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3156 zcmbVO-ESL35a0W-eU3llq)r8Hlhh%J)8e)yX&RAMl(zYxQb^r$eIUeg>`Q#$*k-5~V~i(#_rO z&CJfu{APCddp$J-sPAjz?ZXa2{=h+n#PY}b7qS)F*7TLsm6Ib-9 z>C0Er3Qf_-=yqT91J6|j$ba?h-)%h8Lp!Y43D5dPVEZ6C202gq z4`LLbdb?eeo5|Do${S_T;uh+{Oafgd27vC&VPZv;gh{uca<{qRh{$-VFuSHCXfn-Q zBPY(CJ3knE{f&V^Rx>m({Ce#4$rI;J1GxC;Atof~q=t!aZDQ_xLet{!C^K3#Kf|1T zlS)>(oTtaxpsK|$so7NSQXEC}G|f#DJQR5_`>%}J$BP`phd0`Xx_B}%^bC13jw`>gnX zylXcHVHcP!nN~9?RzH!W$|U4Jo2uko(I^HCWb7757(bEU=-?|>j&^P98*Ktz+?RF| z*b{?1aQbeAJ`I(e9R+8H@#>ny*)DL8?ZoYy?Yx727QNlKI&XF^iQPAlU9Zj-UEMII zyTML!V=se1TmA19Kn!$_YHxfR_)8#9!=vTeBG8*;VH7)#Nisji9jIy%Qc2e)34TWj zaYqShM+yIq5>4Al#JC>G+RF$Wbz7-&Amv3k84zq+NERzQhOW#%#7IOWCQjv28WW&j zFq@WECv+&yjLyWQPBSnwomVKcrDyOZPAPgKVxt&QiuJ(6@mwy$f{AKl(PlJVnTjjs zVdqk^$y`cFVKV}?y{O@W@AynE?aN1^SXlS{$wW+De!dk#GfcjOPLm+~P z*RBFH$clgO!uv)4{;NaAsTFtqvZr?0?OX8%=DHS|=a0^2SKR)&`UP%&_w4YBr*^Jo zp?1D~c5I`7DZ6jJcQ$7U1m+{N@xN-F9{b|}aqKDBLq+@E)jDFUSq&0*pyb+9aP3)8 zimsL=vBiQZcv2+{D*|vGyS_nU(9+Gyr|7iB5ZcxT#i@jvr7yv_10Hkx2Dv9nrrGn* zA8pxOd|FLnuq%WyG8Z@mOTjHb*!C7st$~UfRzoHKh>$UUvQ+UH&dBC+ zI|XlG&u}-{N+Zy4w~}moYQemw2@i42Va6Q??0d?`p_)Q?F=0~C%|-^e&u3JfBG#Dq z5><62uIJ)b@2p8Ab@d`lB*s}FtEiLd@f-y-m(SLSlZkj5GrM&SWPTlnie5Q-)f85Z zrdj3SOsP}>d$}x);$n7yI=bR0myU+fY6XIts8of=x>&|7`}7n@psw~2FlfA<+LEWS z;At#*!Ua!wsimvv={EY8{q=Ka=N-n-av(VO?tGhZ=7Ga=?ZbPLcf}D}kV}r1-yAK= z!RAu1tq^Qm6z&EOT<>2Awl4;Y!K2p)mI3(g=7PKVtG2uDj&&PpcyU!CPR}jhr@oTZ zQII+od+$nC*N(U-UddVAJF2cLSuLthtI_ghm?AD&0RPNHv8>H~VlP05Tb%_{Yo@`B zzgZpTCbt%k)Ca9=I-?w?1E7I*ryT>fDsUY4J8}L=qW4M9s+;%@8m=|T$sJkS?crWn fYvMTPBZ=_-xzru*`A58)JF+eiKD35kX5sw | " or " | Nvim", and "" / "Nvim" with no +# file). kitty repaints the tab bar whenever that title changes, so the tab +# tracks buffer switches for free. from __future__ import annotations import os -import subprocess -import tempfile -import time from typing import Any -from kitty.boss import get_boss -from kitty.fast_data_types import add_timer - -# Nerd Font glyphs (VictorMono Nerd Font Mono covers all of these). +# Nerd Font glyphs; VictorMono Nerd Font Mono covers both. FOLDER = '\uf07b' # nf-fa-folder -BRANCH = '\ue0a0' # nf-pl-branch -AHEAD = '\u21e1' -BEHIND = '\u21e3' +NVIM = '\ue62b' # nf-custom-neovim -# A cached git summary is reused for this long; a working dir that stopped being -# displayed is dropped after PRUNE_AFTER so closed tabs stop being polled. -TTL = 4.0 -PRUNE_AFTER = 30.0 -POLL_INTERVAL = 1.0 -MAX_CONCURRENT = 4 -KILL_AFTER = 15.0 -MAX_DIR_LEN = 24 +EDITORS = frozenset({'nvim', 'vim', 'nvim-qt'}) +MAX_LEN = 28 -# wd -> git summary spec: (branch, ahead, behind, staged, unstaged, untracked, -# conflicts) or None when wd is not a work tree. -_git: dict[str, tuple[str, int, int, int, int, int, int] | None] = {} -_fresh_until: dict[str, float] = {} -_wanted: dict[str, float] = {} -_running: dict[str, tuple[subprocess.Popen[bytes], Any, float]] = {} -_timer = 0 +# 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. +_repo: dict[str, str] = {} def draw_title(data: dict[str, Any]) -> str: - global _timer - if not _timer: - _timer = add_timer(_tick, POLL_INTERVAL, True) - wd = data['tab'].active_wd - fmt = data['fmt'] - parts = [str(data['index'])] + tab = data['tab'] + label = '' + if tab.active_exe in EDITORS: + label = _nvim_file(data['title']) + if label: + return f'{data["index"]} {NVIM} {_elide(label)}' + wd = tab.active_wd if wd: - _wanted[wd] = _now() - parts.append(f'{FOLDER} {_dir_label(wd)}') - summary = _git.get(wd) - if summary is not None: - parts.append(_render_git(summary, fmt)) - else: - parts.append(data['title']) - return ' '.join(parts) + 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"])}' -def _dir_label(wd: str) -> str: - home = os.path.expanduser('~') - if wd == home: - return '~' - name = os.path.basename(wd.rstrip('/')) or wd - if len(name) > MAX_DIR_LEN: - name = name[: MAX_DIR_LEN - 1] + '…' - return name +def _repo_root(wd: str) -> str: + root = _repo.get(wd) + if root is None: + root = '' + path = wd + while True: + if os.path.exists(os.path.join(path, '.git')): + root = path + break + parent = os.path.dirname(path) + if parent == path: + break + path = parent + _repo[wd] = root + return root -def _render_git( - summary: tuple[str, int, int, int, int, int, int], fmt: Any -) -> str: - branch, ahead, behind, staged, unstaged, untracked, conflicts = summary - # color2/3/4/1 are palette slots, so the active Omarchy theme picks the - # actual greens and reds; fg.tab restores the tab's own foreground so the - # powerline chrome keeps drawing in its own colors. - tab = fmt.fg.tab - out = [f'{BRANCH} {branch}' if branch else BRANCH] - for count, mark, color in ( - (ahead, AHEAD, fmt.fg.color6), - (behind, BEHIND, fmt.fg.color6), - (staged, '+', fmt.fg.color2), - (unstaged, '~', fmt.fg.color3), - (untracked, '?', fmt.fg.color4), - (conflicts, '!', fmt.fg.color1), - ): - if count: - out.append(f'{color}{mark}{count}{tab}') - return ' '.join(out) +def _nvim_file(title: str) -> str: + """The file name out of an nvim window title, '' when it is editing none. - -def _now() -> float: - return time.monotonic() - - -def _tick(timer_id: int | None) -> None: - now = _now() - changed = _reap(now) - for wd, last_wanted in tuple(_wanted.items()): - if now - last_wanted > PRUNE_AFTER: - del _wanted[wd] - _git.pop(wd, None) - _fresh_until.pop(wd, None) - continue - if len(_running) >= MAX_CONCURRENT: - break - if wd in _running or _fresh_until.get(wd, 0.0) > now: - continue - _start(wd, now) - if changed: - for tm in get_boss().os_window_map.values(): - tm.mark_tab_bar_dirty() - - -def _start(wd: str, now: float) -> None: - try: - out = tempfile.TemporaryFile() - proc = subprocess.Popen( - ( - 'git', - '--no-optional-locks', - 'status', - '--porcelain=v2', - '--branch', - '--untracked-files=normal', - ), - cwd=wd, - stdout=out, - stderr=subprocess.DEVNULL, - stdin=subprocess.DEVNULL, - # start_new_session, not preexec_fn: kitty is multi-threaded, and - # only the former is implemented without running Python between - # fork and exec. - start_new_session=True, - ) - except OSError: - # No git, or wd vanished: remember "not a repo" so we stop retrying - # until the TTL lapses. - _git[wd] = None - _fresh_until[wd] = now + TTL - return - _running[wd] = (proc, out, now) - - -def _reap(now: float) -> bool: - changed = False - for wd, (proc, out, started) in tuple(_running.items()): - rc = proc.poll() - if rc is None: - if now - started > KILL_AFTER: - proc.kill() - continue - del _running[wd] - summary = None - if rc == 0: - try: - out.seek(0) - summary = _parse(out.read().decode('utf-8', 'replace')) - except OSError: - summary = None - out.close() - if _git.get(wd, ...) != summary: - changed = True - _git[wd] = summary - _fresh_until[wd] = now + TTL - return changed - - -def _parse(text: str) -> tuple[str, int, int, int, int, int, int]: - """git status --porcelain=v2 --branch -> title counts. - - Header lines carry the branch and the ahead/behind pair; `1`/`2` records - carry a two-char XY status (X = index, Y = work tree), `?` is untracked and - `u` is unmerged. Renames (`2`) put the old path after a tab on the same - line, so line-splitting stays correct. + Handles this config's " | " / " | Nvim" and nvim's own + default "%t - Nvim" / " () - NVIM". """ - head = oid = '' - ahead = behind = staged = unstaged = untracked = conflicts = 0 - for line in text.splitlines(): - if line.startswith('# branch.oid '): - oid = line[13:20] - elif line.startswith('# branch.head '): - head = line[14:] - elif line.startswith('# branch.ab '): - for token in line[12:].split(): - if token.startswith('+'): - ahead = int(token[1:] or 0) - elif token.startswith('-'): - behind = int(token[1:] or 0) - elif line[:2] in ('1 ', '2 '): - xy = line[2:4] - if xy[0] != '.': - staged += 1 - if xy[1] != '.': - unstaged += 1 - elif line.startswith('? '): - untracked += 1 - elif line.startswith('u '): - conflicts += 1 - branch = oid if head in ('', '(detached)') else head - return branch, ahead, behind, staged, unstaged, untracked, conflicts + text = title.strip() + for suffix in (' - NVIM', ' - Nvim', ' - nvim'): + if text.endswith(suffix): + text = text[: -len(suffix)] + text = text.split(' | ', 1)[0].strip() + if text.endswith(')') and ' (' in text: + text = text.split(' (', 1)[0].strip() + # Left with a bare repo slug ("org/repo") or the placeholder: no file open. + if not text or '/' in text or text.lower() == 'nvim': + return '' + return text + + +def _elide(label: str) -> str: + return label if len(label) <= MAX_LEN else label[: MAX_LEN - 1] + '…'