diff --git a/.chezmoidata/packages.yaml b/.chezmoidata/packages.yaml index 4e23c43..751019e 100644 --- a/.chezmoidata/packages.yaml +++ b/.chezmoidata/packages.yaml @@ -54,6 +54,7 @@ packages: - xdg-user-dirs aur: - noctalia-shell + - vicinae-bin darwin: brew: - fish diff --git a/.chezmoiexternals/ghostty.toml b/.chezmoiexternals/ghostty.toml new file mode 100644 index 0000000..4f11129 --- /dev/null +++ b/.chezmoiexternals/ghostty.toml @@ -0,0 +1,6 @@ +[".config/ghostty"] + type = "git-repo" + url = "https://github.com/prdlk/ghostty.git" + refreshPeriod = "168h" + [".config/ghostty".pull] + args = ["--ff-only"] diff --git a/.chezmoiexternals/kitty.toml b/.chezmoiexternals/kitty.toml new file mode 100644 index 0000000..ba8b74d --- /dev/null +++ b/.chezmoiexternals/kitty.toml @@ -0,0 +1,6 @@ +[".config/kitty"] + type = "git-repo" + url = "https://github.com/prdlk/kitty.git" + refreshPeriod = "168h" + [".config/kitty".pull] + args = ["--ff-only"] diff --git a/dot_config/ghostty/config b/dot_config/ghostty/config deleted file mode 100644 index b09b93b..0000000 --- a/dot_config/ghostty/config +++ /dev/null @@ -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 diff --git a/dot_config/ghostty/config.ghostty b/dot_config/ghostty/config.ghostty deleted file mode 100644 index c6941e0..0000000 --- a/dot_config/ghostty/config.ghostty +++ /dev/null @@ -1 +0,0 @@ -theme = cyberdream diff --git a/dot_config/ghostty/themes/cyberdream b/dot_config/ghostty/themes/cyberdream deleted file mode 100644 index b7a65af..0000000 --- a/dot_config/ghostty/themes/cyberdream +++ /dev/null @@ -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 diff --git a/dot_config/ghostty/themes/ii-auto b/dot_config/ghostty/themes/ii-auto deleted file mode 100644 index a53e5ca..0000000 --- a/dot_config/ghostty/themes/ii-auto +++ /dev/null @@ -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 diff --git a/dot_config/ghostty/themes/koda-dark b/dot_config/ghostty/themes/koda-dark deleted file mode 100644 index 05048c7..0000000 --- a/dot_config/ghostty/themes/koda-dark +++ /dev/null @@ -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 diff --git a/dot_config/ghostty/themes/koda-light b/dot_config/ghostty/themes/koda-light deleted file mode 100644 index 15563d4..0000000 --- a/dot_config/ghostty/themes/koda-light +++ /dev/null @@ -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 diff --git a/dot_config/kitty/day_session.py b/dot_config/kitty/day_session.py deleted file mode 100644 index e6e67df..0000000 --- a/dot_config/kitty/day_session.py +++ /dev/null @@ -1,246 +0,0 @@ -"""Daily kitty sessions, archived per week. - - ~/.local/share/kitty-sessions//.kitty-session - ~/.local/share/kitty-sessions//.scrollback/.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 ` (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}') diff --git a/dot_config/kitty/goliath.conf b/dot_config/kitty/goliath.conf deleted file mode 100644 index 67723ff..0000000 --- a/dot_config/kitty/goliath.conf +++ /dev/null @@ -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 diff --git a/dot_config/kitty/herdr.conf b/dot_config/kitty/herdr.conf deleted file mode 100644 index d6fd2b1..0000000 --- a/dot_config/kitty/herdr.conf +++ /dev/null @@ -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 diff --git a/dot_config/kitty/kitty.conf b/dot_config/kitty/kitty.conf deleted file mode 100644 index b9b16f0..0000000 --- a/dot_config/kitty/kitty.conf +++ /dev/null @@ -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 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//.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//kitty.conf). Last include wins, so keep this -# at the bottom. -include ~/.local/state/omarchy/current/theme/kitty.conf - -font_size 9.0 diff --git a/dot_config/kitty/navigate_kitty.py b/dot_config/kitty/navigate_kitty.py deleted file mode 100644 index 63f0f66..0000000 --- a/dot_config/kitty/navigate_kitty.py +++ /dev/null @@ -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]) diff --git a/dot_config/kitty/pass_keys.py b/dot_config/kitty/pass_keys.py deleted file mode 100644 index 0ef92a0..0000000 --- a/dot_config/kitty/pass_keys.py +++ /dev/null @@ -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 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) diff --git a/dot_config/kitty/select_tab.py b/dot_config/kitty/select_tab.py deleted file mode 100644 index 561af79..0000000 --- a/dot_config/kitty/select_tab.py +++ /dev/null @@ -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() diff --git a/dot_config/kitty/ssh.conf b/dot_config/kitty/ssh.conf deleted file mode 100644 index 03e8660..0000000 --- a/dot_config/kitty/ssh.conf +++ /dev/null @@ -1,5 +0,0 @@ -hostname goliath - share_connections yes - # optional niceties: - # copy .config/nvim .config/tmux - # interpreter /usr/bin/fish diff --git a/dot_config/kitty/switchtab.py b/dot_config/kitty/switchtab.py deleted file mode 100644 index 3b2ddd3..0000000 --- a/dot_config/kitty/switchtab.py +++ /dev/null @@ -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 diff --git a/dot_config/kitty/symlink_current-theme.conf b/dot_config/kitty/symlink_current-theme.conf deleted file mode 100644 index bc71d66..0000000 --- a/dot_config/kitty/symlink_current-theme.conf +++ /dev/null @@ -1 +0,0 @@ -themes/cyberdream.conf diff --git a/dot_config/kitty/tab_bar.py b/dot_config/kitty/tab_bar.py deleted file mode 100644 index 311717e..0000000 --- a/dot_config/kitty/tab_bar.py +++ /dev/null @@ -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 `