chore(config): externalize Ghostty and Kitty configs, add vicinae-bin package and MIME handler

This commit is contained in:
Prad Nukala committed 2026-10-16 11:50:52 -04:00
1 parent 09de712152
commit de55c943c4
27 files changed
+151 -2163

No files matched your search

-108
View File
@@ -1,108 +0,0 @@
# Font Configuration
font-size = 11
font-family = VictorMono Nerd Font
background-blur = true
# Window Configuration
window-decoration = false
window-padding-x = 0
window-padding-y = 0
background-opacity = 0.9
# Cursor Configuration
cursor-style = underline
cursor-style-blink = true
# Scrollback
scrollback-limit = 3023
# Terminal features
mouse-hide-while-typing = true
copy-on-select = false
confirm-close-surface = false
# Disable annoying copied to clipboard
app-notifications = no-clipboard-copy,no-config-reload
# Ghostty is only a tmux client: tmux owns tabs (windows), splits (panes),
# sessions and persistence. `clear` drops every Ghostty default (tabs,
# splits, alt+N goto_tab, ctrl+alt+arrow goto_split) so those chords reach
# tmux; the ctrl+shift chords below drive tmux through its prefix2
# (\x02 = ctrl+b). Keep `clear` above every other keybind line.
keybind = clear
keybind = ctrl+shift+c=copy_to_clipboard
keybind = ctrl+shift+v=paste_from_clipboard
keybind = ctrl+plus=increase_font_size:1
keybind = ctrl+minus=decrease_font_size:1
keybind = ctrl+zero=reset_font_size
keybind = ctrl+shift+period=reload_config
# shift+enter as CSI-u (ESC[13;2u), the newline-in-prompt key coding agents
# (omp, claude) read. Never \n: that is ctrl+j, tmux's pane-down key.
keybind = shift+enter=text:\x1b[13;2u
# command palette: ctrl+space is tmux's root-table C-Space (tmux-palette);
# sent explicitly as NUL so no keyboard-protocol mode can turn it into a CSI.
# ctrl+backspace is a second key for the same palette.
keybind = ctrl+space=text:\x00
keybind = ctrl+backspace=text:\x00
# project tasks palette (pj): tmux prefix t
keybind = ctrl+shift+p=text:\x02t
keybind = ctrl+shift+space=text:\x02t
# launchers (kitty's ctrl+shift chords), tmux prefix y/e/g/a
keybind = ctrl+shift+f=text:\x02y
keybind = ctrl+shift+e=text:\x02e
keybind = ctrl+shift+g=text:\x02g
keybind = ctrl+shift+a=text:\x02a
# tabs → tmux windows
keybind = ctrl+shift+t=text:\x02c
keybind = ctrl+shift+w=text:\x02k
keybind = ctrl+shift+r=text:\x02r
keybind = ctrl+tab=text:\x02n
keybind = ctrl+shift+tab=text:\x02p
keybind = ctrl+page_down=text:\x02n
keybind = ctrl+page_up=text:\x02p
keybind = ctrl+shift+arrow_right=text:\x02n
keybind = ctrl+shift+arrow_left=text:\x02p
keybind = ctrl+shift+k=text:\x02n
keybind = ctrl+shift+h=text:\x02p
# splits → tmux panes
keybind = ctrl+shift+l=text:\x02v
keybind = ctrl+shift+j=text:\x02h
keybind = ctrl+shift+x=text:\x02x
# session picker: tmux-sessionx on prefix o
keybind = ctrl+shift+o=text:\x02o
keybind = super+enter=text:\x02o
keybind = ctrl+shift+enter=text:\x02z
keybind = ctrl+shift+z=text:\x02z
# scrollback, help (search scrollback: tmux prefix /)
keybind = ctrl+shift+page_up=text:\x02[
keybind = ctrl+shift+slash=text:\x02?
keybind = ctrl+shift+alt+,=text:\x02q
# sessions: quitting Ghostty detaches; the session lives on in tmux
keybind = ctrl+shift+n=text:\x02C
keybind = ctrl+shift+s=text:\x02s
keybind = ctrl+shift+q=text:\x02d
# Tab configuration
gtk-titlebar = false
quit-after-last-window-closed = false
quit-after-last-window-closed-delay = 5m
# Every window gets its own tmux session (~/.local/bin/ghostty-tmux): it
# reclaims the oldest detached session or starts a new one. The first window
# after boot starts the tmux server, and tmux-continuum restores the last
# saved sessions, so closing Ghostty (or rebooting) never loses the shells.
command = shell:$HOME/.local/bin/ghostty-tmux
# Shell integration
shell-integration = detect
shell-integration-features = cursor,sudo,title,no-cursor
# Rando stuff
gtk-single-instance = true
-1
View File
@@ -1 +0,0 @@
theme = cyberdream
-23
View File
@@ -1,23 +0,0 @@
# cyberdream theme for ghostty
palette = 0=#16181a
palette = 1=#ff6e5e
palette = 2=#5eff6c
palette = 3=#f1ff5e
palette = 4=#5ea1ff
palette = 5=#bd5eff
palette = 6=#5ef1ff
palette = 7=#ffffff
palette = 8=#3c4048
palette = 9=#ff6e5e
palette = 10=#5eff6c
palette = 11=#f1ff5e
palette = 12=#5ea1ff
palette = 13=#bd5eff
palette = 14=#5ef1ff
palette = 15=#ffffff
background = #16181a
foreground = #ffffff
cursor-color = #ffffff
selection-background = #3c4048
selection-foreground = #ffffff
-43
View File
@@ -1,43 +0,0 @@
# Auto-generated by ii wallpaper theming system
# Do not edit manually - changes will be overwritten
background = #1D1D1D
foreground = #DFE3E3
cursor-color = #DFE3E3
cursor-text = #1D1D1D
selection-background = #DFE3E3
selection-foreground = #1D1D1D
# Black
palette = 0=#1D1D1D
palette = 8=#8F9192
# Red
palette = 1=#FF9BAD
palette = 9=#FFCFD7
# Green
palette = 2=#00FD98
palette = 10=#C1FFD5
# Yellow
palette = 3=#FFF17B
palette = 11=#FFF39B
# Blue
palette = 4=#6DD0FF
palette = 12=#DAF0FF
# Magenta
palette = 5=#D7B0FF
palette = 13=#F0DFFF
# Cyan
palette = 6=#3DF0F5
palette = 14=#B8FDFF
# White
palette = 7=#ADAAA2
palette = 15=#DFE3E3
-32
View File
@@ -1,32 +0,0 @@
# Forces bold text to use the bright color palette
bold-is-bright = true
# Background and foreground
background = #101010
foreground = #ffffff
# Cursor colors
cursor-color = #b0b0b0
cursor-text = #101010
# Selection colors
selection-background = #272727
selection-foreground = #b0b0b0
# ANSI color palette (0-15)
palette = 0=#101010
palette = 1=#ff7676
palette = 2=#a3d6a3
palette = 3=#ffffff
palette = 4=#b3b3b3
palette = 5=#f4b8e4
palette = 6=#fafafa
palette = 7=#a5adce
palette = 8=#50585d
palette = 9=#ff5733
palette = 10=#86cd82
palette = 11=#d9ba73
palette = 12=#ffffff
palette = 13=#f2a4db
palette = 14=#5abfb5
palette = 15=#b5bfe2
-32
View File
@@ -1,32 +0,0 @@
# Forces bold text to use the bright color palette
bold-is-bright = true
# Background and foreground
background = #faf9f5
foreground = #101010
# Cursor colors
cursor-color = #6a6a6a
cursor-text = #ebebeb
# Selection colors
selection-background = #ebebeb
selection-foreground = #101010
# ANSI color palette (0-15)
palette = 0=#ebebeb
palette = 1=#ca0043
palette = 2=#407f00
palette = 3=#926200
palette = 4=#3d3d3d
palette = 5=#a200d1
palette = 6=#007d7d
palette = 7=#6a6a6a
palette = 8=#969ba5
palette = 9=#f30052
palette = 10=#4f9a00
palette = 11=#b07700
palette = 12=#000000
palette = 13=#c301fb
palette = 14=#009797
palette = 15=#101010
-246
View File
@@ -1,246 +0,0 @@
"""Daily kitty sessions, archived per week.
~/.local/share/kitty-sessions/<monday YYYY-MM-DD>/<day YYYY-MM-DD>.kitty-session
~/.local/share/kitty-sessions/<monday YYYY-MM-DD>/<day YYYY-MM-DD>.scrollback/<window key>.ansi
One file, three entry points (kitty runs it under a different __name__ each):
- geninclude (__main__): creates today's session file and points
startup_session at it whenever no running kitty owns today's session (an
flock held for the owner's lifetime). Every launch after kitty was closed
reopens the day; a second concurrent instance starts plain instead of
duplicating today's tabs. `kitty --session <past day>` (the kitty-days tv
cable) opens that day as a read-only snapshot instead.
- watcher (__kitty_watcher__): SAVE_DELAY seconds after a window opens,
closes, moves, gains focus or finishes a command (and every SAVE_EVERY
seconds, for output from long-running programs), rewrites the session this
instance started with: tabs/splits/cwds, each shell window's scrollback, and
the program running in it. On restore the shell `cat`s the scrollback, then
re-runs the program.
- kitten `day_session.py flush` (sent over remote control by kitty-day-save
before shutdown/reboot): saves right away, then holds further saves for
FREEZE seconds so windows dying during shutdown can't shrink the saved day.
"""
import fcntl
import json
import os
import re
import shlex
import sys
import tempfile
from datetime import date, timedelta
from time import monotonic
from kittens.tui.handler import result_handler
ROOT = os.path.expanduser('~/.local/share/kitty-sessions')
SAVE_DELAY = 1 # seconds; batches bursts of events, and the most change lost when kitty dies
SAVE_EVERY = 30 # seconds; catches output that arrives without any window event
FREEZE = 60 # seconds without saves after a flush; long enough for any shutdown, short if it's cancelled
SB_VAR = 'day_scrollback' # user var naming a window's scrollback dump; kitty saves/restores it via launch --var
UNSERIALIZE = 'kitty-unserialize-data=' # kitty.constants.unserialize_launch_flag
# Never replay a command that was mid-run when the session was saved if any word
# of it is one of these: re-running them on the next launch can destroy data.
NO_REPLAY = {
'rm', 'rmdir', 'mv', 'dd', 'shred', 'truncate', 'wipefs', 'fdisk', 'parted',
'sudo', 'doas', 'su', 'run0', 'pkexec', 'kill', 'pkill', 'killall',
'reboot', 'shutdown', 'poweroff',
}
SGR = re.compile(r'\x1b\[[0-9;:]*m')
PROMPT_MARK = '\x1b]133;A' # shell integration's start-of-prompt mark
_schedule_save = None # set by on_load when this instance owns a day session
def today_path() -> str:
d = date.today()
return os.path.join(ROOT, str(d - timedelta(days=d.weekday())), f'{d}.kitty-session')
def own_day(path: str) -> bool:
# Lock a side file, not the session itself: atomic_save replaces that inode.
# The fd stays open (and non-inheritable) so the lock dies with this kitty.
lock = os.path.join(os.environ.get('XDG_RUNTIME_DIR') or tempfile.gettempdir(),
f'kitty-day-{os.path.basename(path)}.lock')
fd = os.open(lock, os.O_RDONLY | os.O_CREAT, 0o600)
try:
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
os.close(fd)
return False
return True
def replay_safe(cmd: str) -> bool:
try:
words = shlex.split(cmd)
except ValueError:
return False
return not any((b := os.path.basename(w)) in NO_REPLAY or b.startswith('mkfs') for w in words)
def scrollback(window) -> str:
from kitty.window import as_text
s = window.screen
# While a full-screen program (nvim) is up, the shell's output lives in the other buffer.
text = as_text(s, as_ansi=True, add_history=True, alternate_screen=s.is_using_alternate_linebuf())
if window.at_prompt and (i := text.rfind(PROMPT_MARK)) >= 0:
text = text[:i] # the idle prompt; the restored shell draws a fresh one
lines = text.split('\n')
while lines and not SGR.sub('', lines[-1]).strip(): # the unused rows below the cursor
lines.pop()
return '\n'.join(lines) + '\x1b[m\n' if lines else ''
def on_load(boss, data) -> None:
from uuid import uuid4
from kitty.config import atomic_save
from kitty.fast_data_types import add_timer, get_options
from kitty.session import parse_save_as_options_spec_args, session_arg_to_name
global _schedule_save
path = get_options().startup_session or ''
if not path.startswith(ROOT + os.sep):
return
name = session_arg_to_name(path)
sb_dir = path.rpartition('.')[0] + '.scrollback'
ser_opts = parse_save_as_options_spec_args(
['--use-foreground-process', f'--match=session:^{re.escape(name)}$'])[0]
last = ''
dumps: dict[str, str] = {}
pending = False
def launch_parts(line: str) -> tuple[list[str], dict] | None:
args = shlex.split(line) if line.startswith('launch ') else []
if len(args) < 2 or not args[1].startswith(UNSERIALIZE):
return None
return args, json.loads(args[1][len(UNSERIALIZE):])
# Per-window shell startup command (scrollback cat + program), by SB_VAR key.
# Seeded from the file so restored windows keep theirs until their shell is up.
startup: dict[str, str] = {}
with open(path) as f:
for args, data in filter(None, map(launch_parts, f.read().splitlines())):
for a in args:
if a.startswith(f'--var={SB_VAR}=') and data.get('cmd_at_shell_startup'):
startup[a.removeprefix(f'--var={SB_VAR}=')] = data['cmd_at_shell_startup']
def startup_cmd(line: str) -> str:
# Rewrite kitty's per-window restore data: cat the scrollback first, then
# re-run the foreground program unless it is on the NO_REPLAY list.
parts = launch_parts(line)
if parts is None:
return line
args, data = parts
w = boss.window_id_map.get(data['id'])
key = w.user_vars.get(SB_VAR, '') if w else ''
fg = data.pop('cmd_at_shell_startup', None)
# Until the shell draws a prompt, kitty's "foreground process" is shell
# init (mise/direnv hooks) or the replayed program: keep the previous value.
if w and w.screen.last_reported_cwd:
steps = []
dump = os.path.join(sb_dir, f'{key}.ansi')
if os.path.exists(dump):
steps.append(f'cat {shlex.quote(dump)}')
if fg:
fg = fg if isinstance(fg, str) else shlex.join(fg)
if replay_safe(fg):
steps.append(fg)
startup[key] = '; '.join(steps)
if startup.get(key):
data['cmd_at_shell_startup'] = startup[key]
args[1] = UNSERIALIZE + json.dumps(data)
return shlex.join(args)
frozen_until = 0.0
def save(timer_id: int) -> None:
nonlocal last, pending
pending = False
windows = [w for w in boss.all_windows if w.created_in_session_name == name]
# Nothing left in the session means kitty is closing: keep the last good save.
if not windows or monotonic() < frozen_until:
return
os.makedirs(sb_dir, exist_ok=True)
keep = set()
for w in windows:
key = w.user_vars.get(SB_VAR)
if not key:
key = uuid4().hex
w.set_user_var(SB_VAR, key)
keep.add(f'{key}.ansi')
# Only once the shell has drawn a prompt (and reported its cwd): a
# restored window cats its old dump before that, so writing earlier
# could replace the history before it was replayed.
if w.screen.last_reported_cwd:
text = scrollback(w)
if dumps.get(key) != text:
atomic_save(text.encode(), os.path.join(sb_dir, f'{key}.ansi'))
dumps[key] = text
for f in set(os.listdir(sb_dir)) - keep: # windows closed since the last save
os.remove(os.path.join(sb_dir, f))
text = '\n'.join(map(startup_cmd, boss.serialize_state_as_session(path, ser_opts)))
if text != last:
atomic_save(text.encode(), path)
last = text
def schedule() -> None:
nonlocal pending
if not pending:
pending = True
add_timer(save, SAVE_DELAY, False)
def flush() -> None:
nonlocal frozen_until
frozen_until = 0.0
save(0)
frozen_until = monotonic() + FREEZE
_schedule_save = schedule
boss.day_session_flush = flush # the flush kitten runs in its own module copy, so it finds this via boss
add_timer(lambda timer_id: schedule(), SAVE_EVERY, True)
def _changed(boss, window, data) -> None:
if _schedule_save is not None:
_schedule_save()
# on_resize covers windows/tabs being created, closed or re-split; on_cmd_startstop
# catches `cd` and finished commands. Title changes are skipped: spinners would
# re-save every second.
on_resize = on_close = on_focus_change = on_cmd_startstop = _changed
def main(args: list[str]) -> None:
pass
@result_handler(no_ui=True)
def handle_result(args: list[str], answer: str, target_window_id: int, boss) -> None:
if 'flush' in args and (flush := getattr(boss, 'day_session_flush', None)):
flush()
def session_arg() -> str | None:
"""`--session` from kitty's own command line (geninclude runs inside kitty)."""
argv = sys.argv[1:]
for i, a in enumerate(argv):
if a.startswith('--session='):
return a.partition('=')[2]
if a == '--session' and i + 1 < len(argv):
return argv[i + 1]
return None
if __name__ == '__main__':
path = today_path()
other = session_arg()
# An explicit --session for another day must not claim today's lock.
if (other is None or os.path.abspath(os.path.expanduser(other)) == path) and own_day(path):
os.makedirs(os.path.dirname(path), exist_ok=True)
try:
open(path, 'x').close() # empty session = one tab with the default shell
except FileExistsError:
pass
print(f'startup_session {path}')
-39
View File
@@ -1,39 +0,0 @@
listen_on unix:@kitty-goliath
# open straight into goliath's tmux, over mosh: the UDP session survives
# suspend, roaming and dead links, and reconnects itself. mosh-server allocates
# the remote pty, so there is no `ssh -t` equivalent to pass. Caveat: mosh runs
# its bootstrap ssh with `-S none`, so it can never ride a ControlMaster --
# non-interactive key auth to goliath must work or every launch prompts for a
# password. Needs mosh on both ends and UDP 60000-61000 open on goliath.
# mosh also rewrites TERM to xterm-256color (it only forwards a color count),
# so xterm-kitty-only capabilities (undercurl, ...) are gone inside the session.
# MOSH_TITLE_NOPREFIX drops mosh's "[mosh] " window-title prefix.
env MOSH_TITLE_NOPREFIX=1
shell mosh goliath -- tmux new-session -A -s main
# structural keys forward to tmux (prefix C-b = \x02)
map super+w send_text all \x02x
map super+t send_text all \x02%
map super+n send_text all \x02"
map super+enter send_text all \x02t
map alt+h send_text all \x02H
map alt+l send_text all \x02L
map alt+t send_text all \x02t
map alt+n send_text all \x02t
map alt+q send_text all \x021
map alt+w send_text all \x022
map alt+e send_text all \x023
map alt+r send_text all \x024
map alt+t send_text all \x025
map alt+y send_text all \x026
map alt+u send_text all \x027
map alt+i send_text all \x028
map alt+o send_text all \x029
map super+left send_text all \x02\x1b[1;5D
map super+right send_text all \x02\x1b[1;5C
map super+up send_text all \x02\x1b[1;5A
map super+down send_text all \x02\x1b[1;5B
map super+d send_text all \x02d
tab_bar_style hidden
# Colors + transparency from the active Omarchy theme (last include wins)
include ~/.local/state/omarchy/current/theme/kitty.conf
-75
View File
@@ -1,75 +0,0 @@
# ── herdr.conf · kitty as a herdr host ──
# Kitty stays a dumb terminal here: herdr owns panes, tabs and workspaces.
# Keybinds mirror ~/.config/ghostty/config exactly, so both terminals drive
# herdr identically. Prefix ctrl+b == byte \x02; an uppercase letter after the
# prefix byte is the shift variant (\x02N == prefix+shift+n).
# ── base / appearance ──
font_family VictorMono Nerd Font
bold_italic_font auto
window_padding_width 8
hide_window_decorations yes
background_opacity 0.9
copy_on_select auto
strip_trailing_spaces smart
confirm_os_window_close 0
allow_remote_control socket-only
cursor_trail 10
cursor_trail_start_threshold 0
cursor_trail_decay 0.01 0.05
cursor_blink_interval 0
class kitty-herdr
# herdr renders its own sidebar/tab strip — no kitty chrome
tab_bar_style hidden
# bare `herdr` launches-or-attaches the persistent session; closing the window
# detaches, sessions survive (same as ghostty `command = herdr`)
shell herdr
# ── ghostty parity: plain shortcuts ──
map super+c copy_to_clipboard
map super+v paste_from_clipboard
map ctrl+plus change_font_size current +1
map ctrl+minus change_font_size current -1
map ctrl+0 change_font_size current 0
# reload_config moves off ctrl+shift+, (taken by herdr settings below)
map ctrl+shift+alt+, load_config_file
# ── herdr sync: ctrl+shift chords → prefix (\x02 = ctrl+b) bytes ──
map ctrl+shift+n send_text all \x02N
map super+n send_text all \x02G
map super+t send_text all \x02c
map ctrl+shift+t send_text all \x02c
map ctrl+space send_text all \x02t
map super+z send_text all \x02z
map ctrl+tab send_text all \x02.
map ctrl+shift+tab send_text all \x02,
map ctrl+shift+f send_text all \x02K
map ctrl+shift+g send_text all \x02L
# ctrl+shift+e → prefix+e → herdr-nvim toggle; the editor pane opens on the
# left (position = "left" in ~/.config/herdr-nvim/config.toml)
map ctrl+shift+e send_text all \x02e
map ctrl+shift+o send_text all \x02o
map ctrl+shift+h send_text all \x02p
map ctrl+shift+l send_text all \x02n
map ctrl+shift+j send_text all \x02j
map ctrl+shift+k send_text all \x02k
map ctrl+shift+w send_text all \x02W
map ctrl+shift+r send_text all \x02T
map ctrl+shift+, send_text all \x02S
map ctrl+shift+q send_text all \x02x
map ctrl+shift+p send_text all \x02P
# ctrl+h/j/k/l are herdr's vim-herdr-navigation bindings — kitty must not grab
# them, so no split-navigation maps live in this config.
# BEGIN_KITTY_FONTS
font_family family="VictorMono Nerd Font Mono"
bold_font auto
italic_font auto
bold_italic_font auto
# END_KITTY_FONTS
# Colors + transparency from the active Omarchy theme (last include wins)
include ~/.local/state/omarchy/current/theme/kitty.conf
-129
View File
@@ -1,129 +0,0 @@
# ── base.conf · shared ──
font_family VictorMono Nerd Font
font_size 13.0
bold_italic_font auto
window_padding_width 8
hide_window_decorations yes
# Transparency: kitty's own alpha, not the compositor's -- Hyprland fades a
# whole window (chrome included), kitty only makes the cell background
# translucent, so text stays fully opaque on top of it. This alpha is also what
# makes kitty the one window Hyprland blurs: hypr/looknfeel.lua enables blur
# globally and then opts every window out except the terminal classes, and an
# opaque window has no backdrop to blur. kitty's own `background_blur` does
# nothing here -- it rides ext-background-effect-v1, which Hyprland ignores
# while decoration:blur:enabled is false, and is redundant once it is true.
background_opacity 0.9
cursor_blink_interval 1
copy_on_select auto
strip_trailing_spaces smart
confirm_os_window_close 0
allow_remote_control socket-only
# socket-only remote control needs a socket: this also exports KITTY_LISTEN_ON
# into every pane, which is how nvim (config.pane_nav) knows kitty owns the
# panes and how it hands an at-edge <C-h/j/k/l> back to kitty.
listen_on unix:@kitty-{kitty_pid}
cursor_trail 10
cursor_trail_start_threshold 0
cursor_trail_decay 0.01 0.05
cursor_blink_interval 0
enabled_layouts splits:split_axis=vertical,stack
# launch aliases
action_alias launch_mn launch --cwd=current --type=overlay-main
action_alias launch_os launch --cwd=current --type=os-window
action_alias launch_ov launch --cwd=current --type=overlay
action_alias launch_tb launch --cwd=current --type=tab
action_alias launch_hs launch --cwd=current --location=hsplit --bias=25
action_alias launch_vs launch --cwd=current --location=vsplit --bias=33
action_alias launch_vs50 launch --cwd=current --location=vsplit --bias=50
# panes = kitty windows
map super+n launch_vs50
map super+z toggle_layout stack
map ctrl+shift+z toggle_layout stack
map ctrl+shift+t new_tab_with_cwd
map super+enter new_tab_with_cwd
map ctrl+space launch_hs
map ctrl+enter launch_vs
map ctrl+shift+space new_os_window
# Splits
map ctrl+shift+j launch_hs
map ctrl+shift+l launch_vs
map ctrl+shift+h previous_tab
map ctrl+shift+k next_tab
# Shortcuts
map ctrl+shift+g launch_ov lazygit
map ctrl+shift+f launch_tb yazi
# nvim in the current dir, split to the right of the active pane at 50/50
map ctrl+shift+e launch_vs50 nvim
# herdr host: a *separate* kitty process, own config (launch cannot take --config)
# tmux host: same trick — kitty stays dumb, tmux owns panes/windows/sessions
map ctrl+shift+p launch_hs atuin-pick
# Misc
map ctrl+shift+c copy_to_clipboard
map ctrl+shift+v paste_from_clipboard
# tabs
map alt+q goto_tab 1
map alt+w goto_tab 2
map alt+e goto_tab 3
map alt+r goto_tab 4
map alt+t goto_tab 5
map alt+y goto_tab 6
map alt+u goto_tab 7
map alt+i goto_tab 8
map alt+o goto_tab 9
# Daily sessions, archived per week (day_session.py):
# ~/.local/share/kitty-sessions/<monday>/<day>.kitty-session. Launching kitty
# reopens today's session unless another running kitty already owns it: tabs,
# splits, cwds, each shell's scrollback and the program running in it. Tabs
# made with *_with_cwd / --cwd=current join it and are saved ~1s after each
# change, and right before shutdown/reboot (kitty-day-save.service). `tv
# kitty-days` opens any earlier day in a new kitty.
geninclude day_session.py
watcher day_session.py
# split nav (vim-aware)
map ctrl+h kitten pass_keys.py neighboring_window left ctrl+h
map ctrl+j kitten pass_keys.py neighboring_window bottom ctrl+j
map ctrl+k kitten pass_keys.py neighboring_window top ctrl+k
map ctrl+l kitten pass_keys.py neighboring_window right ctrl+l
# resize
map super+left resize_window narrower
map super+right resize_window wider
map super+up resize_window taller
map super+down resize_window shorter 3
tab_bar_style powerline
tab_powerline_style angled
tab_bar_align left
tab_bar_edge top
# always shown, even with one tab: it carries the stack-layout pane indicator
tab_bar_min_tabs 1
# Titles come from ~/.config/kitty/tab_bar.py (`{custom}`): the repo the tab's
# cwd belongs to, or the file nvim is editing when nvim is the active process.
# Active and inactive tabs share the text; the powerline colors mark the focus.
tab_title_template "{custom}"
# BEGIN_KITTY_FONTS
font_family family="VictorMono Nerd Font Mono"
bold_font auto
italic_font auto
bold_italic_font auto
# END_KITTY_FONTS
#
# Colors + transparency come from the active Omarchy theme
# (~/.config/omarchy/themes/<name>/kitty.conf). Last include wins, so keep this
# at the bottom.
include ~/.local/state/omarchy/current/theme/kitty.conf
font_size 9.0
-16
View File
@@ -1,16 +0,0 @@
from kittens.tui.handler import result_handler
from typing import List
from kitty.boss import Boss
def main():
pass
directions = ["right", "left", "top", "bottom"]
@result_handler(no_ui=True)
def handle_result(args: List[str], result: str, target_window_id: int, boss: Boss):
if len(args) == 2 and args[1] in directions:
boss.active_tab.neighboring_window(args[1])
-60
View File
@@ -1,60 +0,0 @@
import re
from kittens.tui.handler import result_handler
from kitty.key_encoding import KeyEvent, parse_shortcut
# Windows whose foreground process owns <C-h/j/k/l> itself: (n)vim and fzf move
# their own splits/selection, tv binds ctrl-h to goto_parent_dir, and tmux/herdr
# run the same vim-aware handoff one layer down (tmux.conf `is_vim`,
# vim-herdr-navigation). Anything else means kitty owns the panes, so kitty
# moves its own window.
PASSTHROUGH = r'\b(vim|nvim|fzf|tv|tmux|herdr)\b'
# Return True if any foreground process in the window matches PASSTHROUGH
def is_window_passthrough(window):
fp = window.child.foreground_processes
# Check if any argument in a process's cmdline matches PASSTHROUGH,
# including when launched via wrappers or pipelines (e.g., "zoxide query | fzf")
def is_process_passthrough(p):
return any(re.search(PASSTHROUGH, arg, re.IGNORECASE) for arg in p['cmdline'])
return any(is_process_passthrough(p) for p in fp)
def encode_key_mapping(window, key_mapping):
mods, key = parse_shortcut(key_mapping)
event = KeyEvent(
mods=mods,
key=key,
shift=bool(mods & 1),
alt=bool(mods & 2),
ctrl=bool(mods & 4),
super=bool(mods & 8),
hyper=bool(mods & 16),
meta=bool(mods & 32),
).as_window_system_event()
return window.encoded_key(event)
def main():
pass
@result_handler(no_ui=True)
def handle_result(args, result, target_window_id, boss):
window = boss.window_id_map.get(target_window_id)
direction = args[2]
key_mapping = args[3]
if window is None:
return
if is_window_passthrough(window):
for keymap in key_mapping.split('>'):
encoded = encode_key_mapping(window, keymap)
window.write_to_child(encoded)
else:
boss.active_tab.neighboring_window(direction)
-628
View File
@@ -1,628 +0,0 @@
#!/usr/bin/env python3
"""
Kitty kitten: select a tab from a list with fuzzy search.
Usage: map some_key kitten select_tab.py
Keys:
Up/Down, Page Up/Page Down - scroll list
Enter - select highlighted tab
Any other input - search box (incremental fuzzy filter)
Ctrl+Up / Ctrl+Down - search history (previous/next)
Ctrl+C / Escape - quit without selecting
"""
from __future__ import annotations
# from kitty.typing_compat import KeyEventType
from kitty.key_encoding import EventType
from kitty.boss import Boss
from kittens.tui.handler import Handler, kitten_ui
from kittens.tui.loop import Loop, debug
from kittens.tui.line_edit import LineEdit
from kittens.tui.operations import styled
import json
import os
import sys
import pprint
from typing import Any
def format_cwd_like_zsh(path: str) -> str:
"""Format CWD like zsh %(4~|…/%3~|%~): full path, or …/last/3/parts if 4+ segments."""
if not path:
return ""
home = os.environ.get("HOME", "") or os.path.expanduser("~")
if home and (path == home or path.startswith(home + "/")):
path = "~" + path[len(home) :].rstrip("/") or ""
path = path.rstrip("/") or "/"
segments = [s for s in path.split("/") if s]
if len(segments) >= 4:
return "…/" + "/".join(segments[-3:])
return path if path else "~"
class TabRow:
"""One row of data for a tab (for display and filtering)."""
def __init__(
self,
tab_id,
os_window_id,
index_in_os_window,
title,
cwd,
vt,
foreground,
search_text="",
):
self.tab_id = tab_id
self.os_window_id = os_window_id
self.index_in_os_window = index_in_os_window
self.title = title
self.cwd = cwd
self.vt = vt
self.foreground = foreground
parts = [title, cwd]
if vt is not None:
parts.append(vt)
parts.append(foreground)
self.search_text = " ".join(parts).lower() if not search_text else search_text
def fuzzy_match(query: str, text: str) -> bool:
"""True if query characters appear in text in order (case-insensitive)."""
if not query:
return True
q = query.lower()
t = text.lower()
i = 0
for c in q:
j = t.find(c, i)
if j < 0:
return False
i = j + 1
return True
def get_fuzzy_match_indices(query: str, text: str) -> list[int]:
"""Indices in text that match each query character in order (case-insensitive)."""
if not query:
return []
q = query.lower()
t = text.lower()
indices = []
i = 0
for c in q:
j = t.find(c, i)
if j < 0:
break
indices.append(j)
i = j + 1
return indices
def highlight_matches(
segment_str: str,
match_indices: list[int],
content_len: int,
) -> str:
"""Wrap matching character runs in segment_str with styled bold cyan (or fallback SGR)."""
in_content = {i for i in match_indices if 0 <= i < content_len}
if not in_content:
return segment_str
# Merge consecutive indices into (start, end) runs
sorted_i = sorted(in_content)
runs = []
start = sorted_i[0]
end = start + 1
for i in sorted_i[1:]:
if i == end:
end = i + 1
else:
runs.append((start, end))
start, end = i, i + 1
runs.append((start, end))
# Build string with highlights
out = []
pos = 0
hi_on = "\033[1;36m" # bold cyan
hi_off = "\033[22;39m"
for s, e in runs:
out.append(segment_str[pos:s])
chunk = segment_str[s:e]
st = styled(chunk, fg="cyan", bold=True)
out.append(st if st else hi_on + chunk + hi_off)
pos = e
out.append(segment_str[pos:])
return "".join(out)
def longest_substring_match_length(query: str, text: str) -> int:
"""Length of longest contiguous substring of query that appears in text (case-insensitive)."""
if not query:
return 0
q = query.lower()
t = text.lower()
best = 0
for start in range(len(q)):
for end in range(start + 1, len(q) + 1):
if q[start:end] in t:
best = max(best, end - start)
return best
def gather_tabs_from_ls(ls_data: list[dict[str, Any]]) -> list[TabRow]:
"""Build a flat list of TabRow from kitten @ ls JSON (list of OS windows)."""
rows: list[TabRow] = []
# Exclude the tab that contains the window we're running in (the overlay's parent)
try:
self_window_id = int(os.environ.get("KITTY_WINDOW_ID", "") or 0)
except (ValueError, TypeError):
self_window_id = 0
for os_win in ls_data:
os_window_id = os_win.get("id", 0)
tabs = os_win.get("tabs") or []
for tab_idx, tab in enumerate(tabs):
windows = tab.get("windows") or []
if self_window_id and any(w.get("id") == self_window_id for w in windows):
continue
tab_id = tab.get("id", 0)
title = tab.get("title") or ""
# Use active/focused window in tab; fallback to first
active = next(
(w for w in windows if w.get("is_focused") or w.get("is_active")),
windows[0] if windows else None,
)
cwd = ""
vt = None
foreground = ""
if active:
debug(f"Active window in tab {tab_id}: {active}")
cwd = active.get("cwd") or ""
user_vars = active.get("user_vars")
if isinstance(user_vars, dict):
vt = user_vars.get("vt")
else:
vt = None
# Foreground process: cmdline or foreground_processes
cmdline = active.get("cmdline")
if cmdline:
foreground = (
" ".join(cmdline) if isinstance(cmdline, list) else str(cmdline)
)
else:
fg_procs = active.get("foreground_processes")
if fg_procs:
parts = []
for p in fg_procs:
if isinstance(p, dict):
pcmd = p.get("cmdline")
if pcmd:
parts.append(
" ".join(pcmd)
if isinstance(pcmd, list)
else str(pcmd)
)
else:
parts.append(str(p))
foreground = " | ".join(parts)
rows.append(
TabRow(
tab_id=tab_id,
os_window_id=os_window_id,
index_in_os_window=tab_idx,
title=title or "(no title)",
cwd=cwd,
vt=vt,
foreground=foreground or "(no process)",
search_text="",
)
)
return rows
class TabSelectHandler(Handler):
"""TUI handler: list of tabs, search box, scroll, history."""
def __init__(self, tab_rows: list[TabRow]) -> None:
super().__init__()
self.all_rows = tab_rows
self.filtered_rows = tab_rows[:]
# self.search_history: list[str] = []
# self.history_index = -1 # -1 = current (not in history)
self.selected_index = 0 # index in filtered list
self.scroll_offset = 0
self.selected_tab_result: dict[str, Any] | None = None # set on Enter
self.line_edit = LineEdit()
# Minimum size so we always draw something (overlay PTY can report 0x0 at start)
MIN_ROWS = 24
MIN_COLS = 80
@property
def screen_rows(self) -> int:
return max(self.MIN_ROWS, getattr(self.screen_size, "rows", 0) or 0)
@property
def screen_cols(self) -> int:
return max(self.MIN_COLS, getattr(self.screen_size, "cols", 0) or 0)
def initialize(self) -> None:
# self.print(f"In TabSelectHandler...")
self.cmd.clear_screen()
self.cmd.set_cursor_visible(True)
self._apply_filter()
self._clamp_selection()
# Draw immediately and also schedule a draw on next event-loop tick. When run
# as an overlay, the PTY size can be 0x0 at first or kitty may only show
# content sent after overlay_ready(); the delayed draw ensures something
# appears once the overlay is ready.
self._draw()
try:
self.asyncio_loop.call_soon(self._draw)
except Exception:
pass
# self.write(SAVE_CURSOR)
# debug("Initialization complete")
def _apply_filter(self) -> None:
# debug("In _apply_filter")
q = self.line_edit.current_input.strip()
if not q:
self.filtered_rows = self.all_rows[:]
else:
q_lower = q.lower()
# Exact VT match: if the search term equals a tab's VT, show only that/those.
vt_exact = [
r for r in self.all_rows if r.vt is not None and str(r.vt).strip() == q
]
if vt_exact:
self.filtered_rows = vt_exact
else:
self.filtered_rows = [
r for r in self.all_rows if fuzzy_match(q_lower, r.search_text)
]
# Sort by longest contiguous substring match (best first), then original order
self.filtered_rows.sort(
key=lambda r: (
-longest_substring_match_length(q_lower, r.search_text),
self.all_rows.index(r),
)
)
self._clamp_selection()
def on_resize(self, screen_size: Any) -> None:
# debug(f"In on_resize with screen_size: {screen_size}")
super().on_resize(screen_size)
self._clamp_selection()
self._draw()
def _clamp_selection(self) -> None:
# debug("In _clamp_selection")
n = len(self.filtered_rows)
if n == 0:
self.selected_index = 0
self.scroll_offset = 0
return
if self.selected_index >= n:
self.selected_index = 0
elif self.selected_index < 0:
self.selected_index = n - 1
# self.selected_index = max(0, min(self.selected_index, n - 1))
# Keep selection in view
list_top = 1
list_bottom = self.screen_rows - 2
list_height = list_bottom - list_top + 1
if self.selected_index < self.scroll_offset:
self.scroll_offset = self.selected_index
elif self.selected_index >= self.scroll_offset + list_height:
self.scroll_offset = self.selected_index - list_height + 1
# def _push_search_to_history(self) -> None:
# debug("In _push_search_to_history")
# if self.search_query.strip() and (
# not self.search_history
# or self.search_history[-1] != self.search_query.strip()
# ):
# self.search_history.append(self.search_query.strip())
# self.history_index = -1
def _draw(self) -> None:
# debug("In _draw")
self.cmd.clear_screen()
rows, cols = self.screen_rows, self.screen_cols
list_top = 1
list_bottom = rows - 2
list_height = list_bottom - list_top + 1
# debug(f"in draw: rows: {rows}, cols: {cols}")
# Column widths (visible chars only). Four segments, three spaces between.
w_vt = 2
remaining = cols - w_vt - 3
w_title = max(12, int(remaining * 0.45))
w_foreground = max(10, int(remaining * 0.35))
w_cwd = max(8, remaining - w_title - w_foreground)
# Header: plain string of exactly cols visible characters, then style once
header_plain = (
"VT".ljust(w_vt)
+ " "
+ "Title".ljust(w_title)
+ " "
+ "Process".ljust(w_foreground)
+ " "
+ "CWD".ljust(w_cwd)
)
self.cmd.set_cursor_position(0, 0)
self.write(styled(header_plain, bold=True) + "\r\n")
# List
q = self.line_edit.current_input.strip().lower()
visible = self.filtered_rows[
self.scroll_offset : self.scroll_offset + list_height
]
def segment_plain(content: str, width: int, add_dots: bool) -> tuple[str, int]:
"""Return (padded segment string, content_len for highlighting)."""
if len(content) > width:
if add_dots and width >= 2:
content = content[: width - 2] + ".."
else:
content = content[:width]
content_len = len(content)
return content.ljust(width), content_len
for i, row in enumerate(visible):
y = list_top + i
global_idx = self.scroll_offset + i
is_selected = global_idx == self.selected_index
# Build each segment as plain text with exact visible width (no SGR yet)
vt_plain, vt_content_len = segment_plain(
row.vt or "-", w_vt, add_dots=False
)
title_plain, title_content_len = segment_plain(
row.title, w_title, add_dots=True
)
fg_plain, fg_content_len = segment_plain(
row.foreground, w_foreground, add_dots=True
)
cwd_display = format_cwd_like_zsh(row.cwd)
cwd_plain, cwd_content_len = segment_plain(
cwd_display, w_cwd, add_dots=True
)
# Apply match highlighting (inserts SGR; visible length unchanged)
if q:
vt_styled = highlight_matches(
vt_plain,
get_fuzzy_match_indices(q, row.vt or ""),
vt_content_len,
)
title_styled = highlight_matches(
title_plain,
get_fuzzy_match_indices(q, row.title),
title_content_len,
)
fg_styled = highlight_matches(
fg_plain,
get_fuzzy_match_indices(q, row.foreground),
fg_content_len,
)
cwd_styled = highlight_matches(
cwd_plain,
get_fuzzy_match_indices(q, cwd_display),
cwd_content_len,
)
else:
vt_styled = vt_plain
title_styled = title_plain
fg_styled = fg_plain
cwd_styled = cwd_plain
# Concatenate segments with single spaces; total visible length is fixed
line_plain = (
vt_styled + " " + title_styled + " " + fg_styled + " " + cwd_styled
)
# If selected, wrap entire line in reverse (no slicing of output)
if is_selected:
rev = styled(line_plain, reverse=True)
line_out = rev if rev else "\033[7m" + line_plain + "\033[27m"
else:
line_out = line_plain
self.cmd.set_cursor_position(0, y)
self.write(line_out)
self.cmd.clear_to_eol()
self.write("\r\n")
# Search line
self.cmd.set_cursor_position(0, rows - 1)
self.line_edit.write(
self.write, prompt="Filter: ", screen_cols=self.screen_size.cols
)
# prompt = "Filter: "
# self.write(prompt + self.search_query)
# self.cmd.clear_to_eol()
# self.cmd.set_cursor_position(len(prompt) + self.search_cursor, rows - 1)
# debug(
# "At end of draw, flushing, current_input: {self.line_edit.current_input}, cursor_pos: {self.line_edit.cursor_pos}"
# )
self.flush()
# def on_key_event(
# self, key_event: KeyEventType, in_bracketed_paste: bool = False
# ) -> None:
# debug(f"Got key event: {key_event}")
# if key_event.text:
# self._on_search_input(key_event.text)
# return
# if self.perform_default_key_action(key_event):
# return
# self.on_key(key_event)
#
def on_key(self, key_event) -> None:
# debug(f"In on_key with key_event: {key_event}")
if self.line_edit.on_key(key_event):
# debug(f"Handled by line_edit: {self.line_edit}")
self.selected_index = 0
self._clamp_selection()
self._apply_filter()
self._draw()
return
if key_event.type is EventType.RELEASE:
return
match key_event.key:
case "ESCAPE":
self.on_interrupt()
return
case "UP":
self.selected_index = self.selected_index - 1
self._clamp_selection()
self._draw()
return
case "DOWN":
self.selected_index = self.selected_index + 1
self._clamp_selection()
self._draw()
return
case "PAGE_UP":
list_height = self.screen_rows - 3
self.selected_index = max(0, self.selected_index - list_height)
self._clamp_selection()
self._draw()
return
case "PAGE_DOWN":
list_height = self.screen_rows - 3
self.selected_index = min(
len(self.filtered_rows) - 1, self.selected_index + list_height
)
self._clamp_selection()
self._draw()
return
case "ENTER":
self._select_current()
return
# if key_event.matches("ctrl+up"):
# self._history_prev()
# return
# if key_event.matches("ctrl+down"):
# self._history_next()
# return
# Line editing in search box
def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
# debug(f"In on_text with text: {text}, in_bracketed_paste: {in_bracketed_paste}")
self.line_edit.on_text(text, in_bracketed_paste)
# debug(
# f"After line_edit.on_text, current_input: {self.line_edit.current_input}, cursor_pos: {self.line_edit.cursor_pos}"
# )
self.selected_index = 0
self._clamp_selection()
self._apply_filter()
self._draw()
# def _history_prev(self) -> None:
# debug("In _history_prev")
# if not self.search_history:
# return
# if self.history_index <= 0:
# if self.search_query.strip():
# self.search_history.append(self.search_query.strip())
# self.history_index = len(self.search_history) - 1
# else:
# self.history_index -= 1
# self.search_query = self.search_history[self.history_index]
# self.search_cursor = len(self.search_query)
# self._apply_filter()
# self._draw()
#
# def _history_next(self) -> None:
# debug("In _history_next")
# if not self.search_history or self.history_index < 0:
# return
# self.history_index += 1
# if self.history_index >= len(self.search_history):
# self.history_index = -1
# self.search_query = ""
# else:
# self.search_query = self.search_history[self.history_index]
# self.search_cursor = len(self.search_query)
# self._apply_filter()
# self._draw()
#
def _select_current(self) -> None:
# debug("In _select_current")
if not self.filtered_rows:
return
row = self.filtered_rows[self.selected_index]
self.selected_tab_result = {
"tab_id": row.tab_id,
"os_window_id": row.os_window_id,
"index": row.index_in_os_window,
}
self.quit_loop(0)
def on_interrupt(self) -> None:
debug("In on_interrupt")
self.quit_loop(1)
on_eot = on_interrupt
@kitten_ui(allow_remote_control=True)
def main(args: list[str]) -> str:
debug("In select_tab.py main")
cp = main.remote_control(["ls"], capture_output=True)
if cp.returncode != 0:
debug(f"Remote control error {cp.returncode}: {cp.stderr}")
sys.stderr.buffer.write(cp.stderr or b"")
raise SystemExit(cp.returncode)
ls_data = json.loads(cp.stdout.decode())
tab_rows = gather_tabs_from_ls(ls_data)
if not tab_rows:
debug("No tab rows?")
raise SystemExit(1)
debug(f"Have tab rows {pprint.pformat(tab_rows)}")
handler = TabSelectHandler(tab_rows)
debug(f"Created handler: {handler}")
sys.stdout.flush()
loop = Loop()
loop.loop(handler)
debug(f"Loop exited, selected_tab_result: {handler.selected_tab_result}")
return json.dumps(handler.selected_tab_result)
# @result_handler(has_ready_notification=True)
def handle_result(
args: list[str],
input: str,
target_window_id: int,
boss: Boss,
) -> None:
data = json.loads(input) if input else None
print("In handle_result with data:", data)
if not data:
return
tab_id = data.get("tab_id")
os_window_id = data.get("os_window_id")
if tab_id is None:
return
# Use Boss API: match tab by id, focus its OS window if needed, then make it active
tabs = list(boss.match_tabs(f"id:{tab_id}"))
if not tabs:
return
tab = tabs[0]
# If the tab is in a different OS window, focus that window first
if os_window_id is not None and hasattr(boss, "focus_os_window"):
try:
boss.focus_os_window(os_window_id)
except Exception:
pass
tab.make_active()
-5
View File
@@ -1,5 +0,0 @@
hostname goliath
share_connections yes
# optional niceties:
# copy .config/nvim .config/tmux
# interpreter /usr/bin/fish
-103
View File
@@ -1,103 +0,0 @@
from kittens.tui.handler import result_handler
from kittens.tui.handler import kitten_ui
# from kittens.tui.loop import debug
from kitty.boss import Boss
from kitty.rc.base import MatchError
from kitty.tabs import Tab, TabManager
from kitty.fast_data_types import swap_tabs
# from pprint import pprint
def get_tab_vt(tab: Tab) -> str | None:
"""Get the 'vt' user var from a tab's windows, or None if not set."""
for window in tab:
vt = window.user_vars.get("vt")
if vt is not None:
return vt
return None
def reorder_tabs_by_vt(tm: TabManager) -> None:
"""Reorder tabs so those without 'vt' come first, then those with 'vt' sorted by value."""
tabs = tm.tabs
if len(tabs) <= 1:
return
# Build list of (tab, vt_value) tuples
tab_vt_pairs: list[tuple[Tab, str | None]] = [
(tab, get_tab_vt(tab)) for tab in tabs
]
# Sort: tabs without vt first (None sorts before strings with key trick),
# then tabs with vt sorted by vt value
def sort_key(pair: tuple[Tab, str | None]) -> tuple[int, int, str]:
tab, vt = pair
if vt is None:
return (0, 0, "") # No vt: sort first
# Try numeric sort first, fall back to string sort
try:
return (1, int(vt), "") # Numeric vt
except ValueError:
return (2, 0, vt) # Non-numeric vt: sort after numeric, by string
sorted_pairs = sorted(tab_vt_pairs, key=sort_key)
sorted_tabs = [pair[0] for pair in sorted_pairs]
# Check if already in correct order
if sorted_tabs == tabs:
return
# Reorder by swapping to target positions
# We'll do a simple bubble-sort style swap to get tabs in place
for target_idx, target_tab in enumerate(sorted_tabs):
current_idx = tabs.index(target_tab)
while current_idx > target_idx:
# Swap with previous position
tabs[current_idx], tabs[current_idx - 1] = (
tabs[current_idx - 1],
tabs[current_idx],
)
swap_tabs(tm.os_window_id, current_idx, current_idx - 1)
current_idx -= 1
tm.mark_tab_bar_dirty()
# @kitten_ui(allow_remote_control=True)
def main(args: list[str]) -> None:
pass
@result_handler(no_ui=True)
def handle_result(
args: list[str], answer: str, target_window_id: int, boss: Boss
) -> None:
target_id = args[1]
# pprint(("target_id: ", target_id))
# pprint(("args: ", args))
w = boss.window_id_map.get(target_window_id)
tm = boss.active_tab_manager_with_dispatch
if tm is None:
raise RuntimeError("No tab manager?")
tabs = list(boss.match_tabs(f"var:vt=^{target_id}$"))
if len(tabs) > 0:
tm.set_active_tab(tabs[0])
return
# pprint(("tm", tm, "tabs", list(tabs)))
if w is not None:
try:
boss.call_remote_control(
w, ("focus-tab", "--match", f"var:vt=^{target_id}$")
)
except MatchError:
# pprint("No match, need to create tab")
boss.call_remote_control(
w, ("launch", "--type", "tab", "--var", f"vt={target_id}", *args[2:])
)
reorder_tabs_by_vt(tm)
tabs = list(boss.match_tabs(f"var:vt=^{target_id}$"))
if len(tabs) > 0:
tm.set_active_tab(tabs[0])
return
@@ -1 +0,0 @@
themes/cyberdream.conf
-252
View File
@@ -1,252 +0,0 @@
# ── 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 `<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`;
# 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 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
STACKED = '\U000f0328' # nf-md-layers — stack layout is hiding the tab's other panes
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': '\uf2db', # nf-fa-microchip
'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': '\U000f10fe', # nf-md-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', '--'})
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+f5) to show up.
_repo: dict[str, str] = {}
def draw_title(data: dict[str, Any]) -> str:
return _title(data) + _stacked(data)
def _title(data: dict[str, Any]) -> str:
tab = data['tab']
program = _program(_cmdlines(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 = '~' 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 _stacked(data: dict[str, Any]) -> str:
"""` <layers icon> 3` when the stack layout (super+z) shows one of 3 panes, else ''.
kitty marks the tab bar dirty on every layout change, so toggling repaints this.
"""
panes = data['num_window_groups']
return f' {STACKED} {panes}' if data['layout_name'] == 'stack' and panes > 1 else ''
def _cmdlines(tab_id: int) -> list[list[str]]:
"""argv of every foreground process of the tab's active window.
All of them, not just `foreground_cmdline`: an editor commonly has LSP/agent
children in the same process group, and kitty's single "process for cwd" can
be either one of them.
"""
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
if window is None:
return []
return [list(p['cmdline'] or ()) for p in window.child.foreground_processes]
except Exception:
return []
def _program(cmdlines: list[list[str]], fallback_exe: str) -> str:
"""Program a tab should be iconified by, across its foreground processes.
An editor wins over anything else (nvim with a node LSP child is nvim), then
the first process with an icon, then whatever the first process is.
"""
names = [name for cmdline in cmdlines if (name := _resolve(cmdline))]
for name in names:
if name in EDITORS:
return name
for name in names:
if name in ICONS:
return name
return names[0] if names else _canonical(os.path.basename(fallback_exe or '').lower())
def _resolve(cmdline: list[str]) -> str:
"""Program name of one cmdline.
Walks 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.
"""
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
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:
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 _nvim_file(title: str) -> str:
"""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".
"""
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] + '…'
-27
View File
@@ -1,27 +0,0 @@
# cyberdream theme for kitty
background #16181a
foreground #ffffff
cursor #ffffff
cursor_text_color #16181a
selection_background #3c4048
color0 #16181a
color8 #3c4048
color1 #ff6e5e
color9 #ff6e5e
color2 #5eff6c
color10 #5eff6c
color3 #f1ff5e
color11 #f1ff5e
color4 #5ea1ff
color12 #5ea1ff
color5 #bd5eff
color13 #bd5eff
color6 #5ef1ff
color14 #5ef1ff
color7 #ffffff
color15 #ffffff
selection_foreground #ffffff
active_tab_foreground #000000
active_tab_background #ffbd5e
inactive_tab_foreground #ffffff
inactive_tab_background #16181a
-74
View File
@@ -1,74 +0,0 @@
# vim:ft=kitty
## name: Koda Dark
## author: Koda
## license: MIT
# The basic colors
foreground #ffffff
background #101010
selection_foreground #b0b0b0
selection_background #272727
# Cursor colors
cursor #b0b0b0
cursor_text_color #101010
# Scrollbar colors
scrollbar_handle_color #b0b0b0
scrollbar_track_color #272727
# URL color when hovering with mouse
url_color #d9ba73
# Kitty window border colors
active_border_color #ffffff
inactive_border_color #777777
bell_border_color #d9ba73
# OS Window titlebar colors
wayland_titlebar_color system
macos_titlebar_color system
# Tab bar colors
active_tab_foreground #ffffff
active_tab_background #101010
inactive_tab_foreground #777777
inactive_tab_background #272727
tab_bar_background #272727
# Colors for marks (marked text in the terminal)
mark1_foreground #121212
mark1_background #b3b3b3
mark2_foreground #121212
mark2_background #f4b8e4
mark3_foreground #121212
mark3_background #5abfb5
# The 16 terminal colors
color0 #101010
color8 #666666
color1 #ff5733
color9 #ff7676
color2 #86cd82
color10 #a3d6a3
color3 #d9ba73
color11 #ffffff
color12 #666666
color4 #b3b3b3
color5 #f2a4db
color13 #f4b8e4
color6 #5abfb5
color14 #fafafa
color7 #a5adce
color15 #b5bfe2
-72
View File
@@ -1,72 +0,0 @@
# vim:ft=kitty
## name: Koda Light
## author: Koda
## license: MIT
# The basic colors
foreground #101010
background #faf9f5
selection_foreground #101010
selection_background #e2e2e2
# Cursor colors
cursor #6a6a6a
cursor_text_color #ebebeb
# Scrollbar colors
scrollbar_handle_color #101010
scrollbar_track_color #e2e2e2
# URL color when hovering with mouse
url_color #b07700
# Kitty window border colors
active_border_color #000000
inactive_border_color #6a6a6a
bell_border_color #b07700
# OS Window titlebar colors
wayland_titlebar_color system
macos_titlebar_color system
# Tab bar colors
active_tab_foreground #101010
active_tab_background #faf9f5
inactive_tab_foreground #969ba5
inactive_tab_background #e2e2e2
tab_bar_background #ebebeb
# Colors for marks (marked text in the terminal)
mark1_foreground #faf9f5
mark1_background #3d3d3d
mark2_foreground #faf9f5
mark2_background #a200d1
mark3_foreground #faf9f5
mark3_background #007d7d
color0 #e2e2e2
color8 #ababab
color1 #ca0043
color9 #f30052
color2 #407f00
color10 #4f9a00
color3 #926200
color11 #b07700
color4 #3d3d3d
color12 #000000
color5 #a200d1
color13 #c301fb
color6 #007d7d
color14 #009797
color7 #6a6a6a
color15 #101010
-100
View File
@@ -1,100 +0,0 @@
# ── tmux.conf · kitty as a tmux host ──
# Kitty is a dumb terminal here: tmux owns panes, windows and sessions.
# Every structural chord from kitty.conf is re-sent as a tmux key sequence
# (prefix ctrl+b == byte \x02); an uppercase letter after the prefix byte is
# the shift variant (\x02H == prefix+shift+h). Actions tmux has no binding
# for run as out-of-band `tmux` client commands (ctrl+shift+g/e/p).
# Launched from the default config with ctrl+shift+t.
# ── base / appearance (kitty.conf parity) ──
font_family VictorMono Nerd Font
bold_italic_font auto
window_padding_width 8
hide_window_decorations yes
background_opacity 0.9
copy_on_select auto
strip_trailing_spaces smart
confirm_os_window_close 0
allow_remote_control socket-only
listen_on unix:@kitty-tmux
cursor_trail 10
cursor_trail_start_threshold 0
cursor_trail_decay 0.01 0.05
cursor_blink_interval 0
# tmux renders its own status line (tmux-dotbar, top) — no kitty chrome
tab_bar_style hidden
# one tmux session per kitty window: cold start boots the server and runs
# tmux-resurrect restore, warm start claims the oldest detached session
shell kitty-tmux
# kitty must not shadow tmux, so every kitty default shortcut is dropped and
# only what tmux cannot do is re-added below
clear_all_shortcuts yes
# ── kitty-level keys (no tmux equivalent) ──
map ctrl+shift+c copy_to_clipboard
map ctrl+shift+v paste_from_clipboard
map ctrl+plus change_font_size current +1
map ctrl+minus change_font_size current -1
map ctrl+0 change_font_size current 0
map ctrl+shift+alt+, load_config_file
map ctrl+shift+a launch --type=background --cwd=current kitty --config ~/.config/kitty/herdr.conf
# ── kitty tabs → tmux windows ──
map super+t send_text all \x02t
map super+enter send_text all \x02t
map ctrl+shift+t send_text all \x02t
map ctrl+shift+h send_text all \x02H
map ctrl+shift+k send_text all \x02L
map alt+q send_text all \x021
map alt+w send_text all \x022
map alt+e send_text all \x023
map alt+r send_text all \x024
map alt+t send_text all \x025
map alt+y send_text all \x026
map alt+u send_text all \x027
map alt+i send_text all \x028
map alt+o send_text all \x029
# ── kitty splits → tmux panes ──
map ctrl+shift+j send_text all \x02"
map ctrl+shift+l send_text all \x02%
map ctrl+enter send_text all \x02%
map ctrl+shift+f send_text all \x02z
map ctrl+shift+w send_text all \x02x
map super+w send_text all \x02x
# resize: prefix + ctrl+arrow == tmux resize-pane (repeatable)
map super+left send_text all \x02\x1b[1;5D
map super+right send_text all \x02\x1b[1;5C
map super+up send_text all \x02\x1b[1;5A
map super+down send_text all \x02\x1b[1;5B
# ctrl+h/j/k/l are tmux's vim-aware pane navigation and ctrl+space is
# tmux-palette — both no-prefix tmux binds, so kitty must not grab them.
# No maps for those live in this config (splits stay on ctrl+shift+j/l).
# ── launchers: no tmux bind exists for these ──
# Driving the prefix command prompt (\x02: … \r) does NOT work: kitty writes
# the whole chord in one pty write and tmux drops the buffered bytes when the
# prompt opens. Run the tmux client command out-of-band instead; with no -c
# it targets the current client, i.e. this kitty window.
map ctrl+shift+g launch --type=background tmux display-popup -w 90% -h 90% -E -d "#{pane_current_path}" lazygit
map ctrl+shift+e launch --type=background tmux new-window -c "#{pane_current_path}" nvim
map ctrl+shift+p launch --type=background tmux split-window -v -l 25% -c "#{pane_current_path}" atuin-pick
# ── sessions ──
# a new kitty OS window runs kitty-tmux again == its own tmux session
map super+n new_os_window
map ctrl+shift+space new_os_window
map super+d send_text all \x02d
# BEGIN_KITTY_FONTS
font_family family="VictorMono Nerd Font Mono"
bold_font auto
italic_font auto
bold_italic_font auto
# END_KITTY_FONTS
# Colors + transparency from the active Omarchy theme (last include wins)
include ~/.local/state/omarchy/current/theme/kitty.conf
-97
View File
@@ -1,97 +0,0 @@
# ~/.config/kitty/mywatcher.py
from kitty.tabs import TabManager
from typing import Any
from kitty.boss import Boss
from kitty.window import Window
# from pprint import pprint
# Kitty currently ships with Python 3.12, but os.ptsname was added in 3.13.
import ctypes
libc = ctypes.CDLL("libc.so.6")
libc.ptsname.restype = ctypes.c_char_p
updating = False
def get_title(window: Window, title: str) -> str:
vt = "n/a"
try:
vt = window.user_vars["vt"]
except KeyError:
pass
ptsname = libc.ptsname(window.child.child_fd).decode()
ptsname = ptsname.removeprefix("/dev/")
return f"{title} [{vt} {ptsname}]"
def on_title_change(boss: Boss, window: Window, data: dict[str, Any]) -> None:
# pprint(("title change", "window", window, "data", data))
tm = boss.active_tab_manager_with_dispatch
if tm is not None:
update_title(boss, window, tm)
def on_tab_bar_dirty(boss: Boss, window: Window, data: dict[str, Any]) -> None:
# called when any changes happen to the tab bar, such a new tabs being
# created, tab titles changing, tabs moving, etc. Useful to display the
# tab bar externally to kitty. This is called even if the tab bar is
# hidden. Note that this is called only in *global watchers*, that is
# watchers defined in kitty.conf or using the --watcher command line
# flag. data contains tab_manager which is the object responsible for
# managing all tabs in a single OS Window.
# return get_title(window, data["title"])
tm = data["tab_manager"]
update_title(boss, window, tm)
def update_title(boss: Boss, window: Window, tm: TabManager) -> None:
# pprint(("update title", "window", window))
global updating
if updating:
# pprint("already updating, skipping")
return
tab = tm.active_tab
if tab is None:
return
title = get_title(window, window.child_title)
if title != tab.effective_title:
updating = True
# pprint(
# (
# "title",
# title,
# "tab.effective title",
# tab.effective_title,
# "window.title",
# window.title,
# "window.child_title",
# window.child_title,
# )
# )
window.set_title(title)
updating = False
else:
pass
# pprint(
# (
# "no update",
# "title",
# title,
# "tab.effective title",
# tab.effective_title,
# "window.title",
# window.title,
# "window.child_title",
# window.child_title,
# )
# )
return
+1
View File
@@ -39,6 +39,7 @@ inode/directory=io.github.lgse.Strata.desktop
x-scheme-handler/fastmail=com.fastmail.Fastmail.desktop
x-scheme-handler/grokbot=grok-bot.desktop
x-scheme-handler/sand=grok-bot.desktop
x-scheme-handler/tern=so.stencil.tern.url.desktop
[Added Associations]
x-scheme-handler/http=brave-origin.desktop;userapp-Floorp-8MTFP3.desktop;
+137
View File
@@ -0,0 +1,137 @@
// This configuration is merged with the default vicinae configuration file, which you can obtain by running the `vicinae config default` command.
// Every item defined in this file takes precedence over the values defined in the default config or any other imported file.
//
// You can make manual edits to this file, however you should keep in mind that this file may be written to by vicinae when a configuration change is made through the GUI.
// When that happens, any custom comments or formatting will be lost.
//
// If you want to maintain a configuration file with your own comments and formatting, you should create a separate file and add it to the 'imports' array.
//
// Learn more about configuration at https://docs.vicinae.com/config
{
"$schema": "https://vicinae.com/schemas/config.json",
"close_on_focus_loss": true,
"pop_to_root_on_close": true,
"escape_key_behavior": "",
"keybinding": "vim",
"global_shortcuts": {
"toggle": "super+SPACE"
},
"font": {
"rendering": "native",
"normal": {
"family": "VictorMono NFP",
"size": 9.5
}
},
"theme": {
"dark": {
"icon_theme": "Yaru"
}
},
"launcher_window": {
"compact_mode": {
"enabled": false
},
"floating_status_bar": false
},
"providers": {
"@aurelleb/store.vicinae.chromium-bookmarks": {
"entrypoints": {
"search-bookmarks": {
"shortcut": "super+control+shift+O"
}
}
},
"@codingcodax/store.vicinae.omarchy-menu": {
"entrypoints": {
"index": {
"shortcut": "alt+SPACE"
}
}
},
"@fbosch/store.vicinae.nerdfont-search": {
"entrypoints": {
"nerdfonts-search": {
"alias": "nf"
}
}
},
"@rithvikvibhu/store.vicinae.arch-packages": {
"entrypoints": {
"search-arch-packages": {
"alias": "aur"
}
}
},
"applications": {
"entrypoints": {
"1password": {
"alias": "pass"
},
"photocraft": {
"alias": "photoshop"
},
"vectorcraft": {
"alias": "illustrator"
}
}
},
"clipboard": {
"entrypoints": {
"history": {
"shortcut": "super+P"
}
}
},
"core": {
"entrypoints": {
"search-emojis": {
"shortcut": "super+shift+SPACE"
}
}
},
"manage-shortcuts": {
"entrypoints": {
"create": {
"alias": "cql"
}
}
},
"power": {
"entrypoints": {
"power-off": {
"alias": "shut down"
},
"reboot": {
"alias": "restart"
}
}
},
"shortcuts": {
"entrypoints": {
"sct-a5e51f1bbe34": {
"alias": "streams"
}
}
},
"system": {
"entrypoints": {
"browse-apps": {
"alias": "apps",
"enabled": true,
"shortcut": "super+A"
},
"run": {
"alias": "sh"
},
"set-default-browser": {
"enabled": false
},
"set-default-terminal": {
"enabled": false
}
}
}
}
}