bc4cb47a4c
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
225 lines
6.4 KiB
Python
225 lines
6.4 KiB
Python
"""Custom kitty tab bar — Sethian status bar.
|
|
|
|
Flat tab style (no powerline separators) + right-aligned status area.
|
|
Shows user@ prefix for non-seth users.
|
|
Status: git branch, CPU% + temp, memory, uptime, hostname, battery, clock.
|
|
Temp and battery hide themselves when the sensor is missing (steel141 has no battery).
|
|
"""
|
|
|
|
import glob
|
|
import os
|
|
import subprocess
|
|
from datetime import datetime
|
|
|
|
from kitty.fast_data_types import add_timer, get_boss
|
|
from kitty.tab_bar import (
|
|
DrawData,
|
|
ExtraData,
|
|
Screen,
|
|
TabBarData,
|
|
as_rgb,
|
|
)
|
|
|
|
_user = os.environ.get("USER", "")
|
|
_prefix = f"{_user}@" if _user and _user != "seth" else ""
|
|
_hostname = os.uname().nodename
|
|
|
|
# Nerd Font icons
|
|
_ICON_BRANCH = "\ue725"
|
|
_ICON_CPU = "\uf4bc"
|
|
_ICON_MEM = "\uf035b"
|
|
_ICON_UP = "\uf0aa"
|
|
_ICON_HOST = "\uf108"
|
|
_ICON_CLOCK = "\uf017"
|
|
_ICON_TEMP = "\uf2c9"
|
|
_ICON_BAT = "\uf240"
|
|
_ICON_CHG = "\uf0e7"
|
|
|
|
# Colors
|
|
_ORANGE = as_rgb(0xD35400)
|
|
_BLACK = as_rgb(0x000000)
|
|
_DIM = as_rgb(0x666666)
|
|
_RED = as_rgb(0xFF3030)
|
|
_TAB_BG = as_rgb(0x1A1A1A)
|
|
_BAR_BG = as_rgb(0x0D0D0D)
|
|
|
|
# kitty only redraws the tab bar on tab changes; this timer keeps the clock/stats live.
|
|
# ponytail: 10s = clock lags <=10s; each redraw runs git on kitty's main thread, so don't go much lower.
|
|
REFRESH_SECS = 10.0
|
|
_timer_id = None
|
|
|
|
|
|
def _refresh(_timer_id) -> None:
|
|
for tm in get_boss().all_tab_managers:
|
|
tm.mark_tab_bar_dirty()
|
|
|
|
|
|
def _get_git_branch() -> str:
|
|
try:
|
|
r = subprocess.run(
|
|
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
capture_output=True, text=True, timeout=1,
|
|
)
|
|
if r.returncode == 0:
|
|
return r.stdout.strip()
|
|
except Exception:
|
|
pass
|
|
return ""
|
|
|
|
|
|
def _get_cpu_percent() -> str:
|
|
try:
|
|
with open("/proc/stat") as f:
|
|
parts = f.readline().split()
|
|
idle = int(parts[4]) + int(parts[5])
|
|
total = sum(int(p) for p in parts[1:])
|
|
prev_idle = getattr(_get_cpu_percent, "_i", 0)
|
|
prev_total = getattr(_get_cpu_percent, "_t", 0)
|
|
_get_cpu_percent._i = idle
|
|
_get_cpu_percent._t = total
|
|
if prev_total == 0:
|
|
return "..."
|
|
dt = total - prev_total
|
|
di = idle - prev_idle
|
|
return f"{(1.0 - di / dt) * 100:.0f}%" if dt else "0%"
|
|
except Exception:
|
|
return "?"
|
|
|
|
|
|
# Intel = coretemp, AMD = k10temp; temp1 is the package/Tctl sensor on both.
|
|
# hwmon numbers move between boots, so resolve once at kitty start.
|
|
_CPU_TEMP = next(
|
|
(os.path.join(os.path.dirname(n), "temp1_input")
|
|
for n in glob.glob("/sys/class/hwmon/hwmon*/name")
|
|
if open(n).read().strip() in ("coretemp", "k10temp")),
|
|
None,
|
|
)
|
|
# BAT* only: mice/keyboards also show up in power_supply as hid*/hidpp_battery*.
|
|
_BAT = next(iter(sorted(glob.glob("/sys/class/power_supply/BAT*"))), None)
|
|
|
|
|
|
def _get_cpu_temp() -> str:
|
|
try:
|
|
with open(_CPU_TEMP) as f:
|
|
return f"{int(f.read()) // 1000}\u00b0"
|
|
except Exception:
|
|
return ""
|
|
|
|
|
|
def _get_battery() -> tuple[str, str, bool]:
|
|
"""(icon, '60%', low) or ('', '', False) when there's no battery."""
|
|
try:
|
|
with open(f"{_BAT}/capacity") as f:
|
|
cap = int(f.read())
|
|
with open(f"{_BAT}/status") as f:
|
|
status = f.read().strip()
|
|
except Exception:
|
|
return "", "", False
|
|
on_ac = status in ("Charging", "Full", "Not charging")
|
|
return (_ICON_CHG if on_ac else _ICON_BAT), f"{cap}%", (not on_ac and cap <= 20)
|
|
|
|
|
|
def _get_memory() -> str:
|
|
try:
|
|
info = {}
|
|
with open("/proc/meminfo") as f:
|
|
for line in f:
|
|
p = line.split()
|
|
info[p[0].rstrip(":")] = int(p[1])
|
|
used = info["MemTotal"] - info.get("MemAvailable", info.get("MemFree", 0))
|
|
return f"{used / 1048576:.0f}/{info['MemTotal'] / 1048576:.0f}G"
|
|
except Exception:
|
|
return "?"
|
|
|
|
|
|
def _get_uptime() -> str:
|
|
try:
|
|
with open("/proc/uptime") as f:
|
|
secs = int(float(f.read().split()[0]))
|
|
d, rem = divmod(secs, 86400)
|
|
h, rem = divmod(rem, 3600)
|
|
m = rem // 60
|
|
if d > 0:
|
|
return f"{d}d {h}h"
|
|
return f"{h}h {m}m" if h > 0 else f"{m}m"
|
|
except Exception:
|
|
return "?"
|
|
|
|
|
|
def draw_tab(
|
|
draw_data: DrawData,
|
|
screen: Screen,
|
|
tab: TabBarData,
|
|
before: int,
|
|
max_tab_length: int,
|
|
index: int,
|
|
is_last: bool,
|
|
extra_data: ExtraData,
|
|
) -> int:
|
|
global _timer_id
|
|
if _timer_id is None:
|
|
_timer_id = add_timer(_refresh, REFRESH_SECS, True)
|
|
|
|
# Build tab title
|
|
title = tab.title
|
|
if _prefix:
|
|
title = f"{_prefix}{title}"
|
|
label = f" {index - 1}: {title} "
|
|
|
|
# Truncate if needed
|
|
if len(label) > max_tab_length:
|
|
label = label[: max_tab_length - 1] + "\u2026"
|
|
|
|
# Draw the tab
|
|
if tab.is_active:
|
|
screen.cursor.fg = _BLACK
|
|
screen.cursor.bg = _ORANGE
|
|
screen.cursor.bold = True
|
|
else:
|
|
screen.cursor.fg = _DIM
|
|
screen.cursor.bg = _TAB_BG
|
|
screen.cursor.bold = False
|
|
|
|
screen.draw(label)
|
|
screen.cursor.bold = False
|
|
|
|
# Small gap between tabs
|
|
screen.cursor.fg = _DIM
|
|
screen.cursor.bg = _BAR_BG
|
|
screen.draw(" ")
|
|
|
|
end = screen.cursor.x
|
|
|
|
# Right-aligned status on last tab
|
|
if is_last:
|
|
sep = " "
|
|
cells: list[tuple[str, int]] = []
|
|
|
|
branch = _get_git_branch()
|
|
if branch:
|
|
cells += [(_ICON_BRANCH + " ", _ORANGE), (branch, _DIM), (sep, _DIM)]
|
|
|
|
cells += [(_ICON_CPU + " ", _ORANGE), (_get_cpu_percent(), _DIM)]
|
|
temp = _get_cpu_temp()
|
|
if temp:
|
|
cells += [(" " + _ICON_TEMP + " ", _ORANGE), (temp, _DIM)]
|
|
cells += [(sep, _DIM)]
|
|
cells += [(_ICON_MEM + " ", _ORANGE), (_get_memory(), _DIM), (sep, _DIM)]
|
|
cells += [(_ICON_UP + " ", _ORANGE), (_get_uptime(), _DIM), (sep, _DIM)]
|
|
cells += [(_ICON_HOST + " ", _ORANGE), (_hostname, _DIM), (sep, _DIM)]
|
|
bat_icon, bat, low = _get_battery()
|
|
if bat:
|
|
cells += [(bat_icon + " ", _RED if low else _ORANGE), (bat, _RED if low else _DIM), (sep, _DIM)]
|
|
cells += [(_ICON_CLOCK + " ", _ORANGE), (datetime.now().strftime("%H:%M"), _DIM), (" ", _DIM)]
|
|
|
|
total_len = sum(len(t) for t, _ in cells)
|
|
right_pos = screen.columns - total_len
|
|
if right_pos > end:
|
|
screen.cursor.x = right_pos
|
|
for text, fg in cells:
|
|
screen.cursor.fg = fg
|
|
screen.cursor.bg = _BAR_BG
|
|
screen.draw(text)
|
|
|
|
return end
|