diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index 3be02c5..368d93a 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -8,3 +8,7 @@ {"id":"int-aca22d17","kind":"field_change","created_at":"2026-08-09T18:51:25.617630581Z","actor":"Prad Nukala","issue_id":"chezmoi-fde","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"starship.toml added from live; packages updated"}} {"id":"int-8975e4ce","kind":"field_change","created_at":"2026-08-09T18:51:54.252398427Z","actor":"Prad Nukala","issue_id":"chezmoi-uw2","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}} {"id":"int-fcc3a2e9","kind":"field_change","created_at":"2026-08-09T18:51:54.41281937Z","actor":"Prad Nukala","issue_id":"chezmoi-sd9","extra":{"field":"status","new_value":"in_progress","old_value":"open"}} +{"id":"int-a40ecd49","kind":"field_change","created_at":"2026-08-09T18:52:20.003755633Z","actor":"Prad Nukala","issue_id":"chezmoi-sd9","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}} +{"id":"int-68e58a83","kind":"field_change","created_at":"2026-08-09T18:52:20.158250426Z","actor":"Prad Nukala","issue_id":"chezmoi-xi5","extra":{"field":"status","new_value":"in_progress","old_value":"open"}} +{"id":"int-7b4d4c0d","kind":"field_change","created_at":"2026-08-09T18:52:41.13963999Z","actor":"Prad Nukala","issue_id":"chezmoi-xi5","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"tmux.conf real content (was symlink_); no vertical geometry present; plugins unmanaged, tpm bootstrap in post script"}} +{"id":"int-e52df1d7","kind":"field_change","created_at":"2026-08-09T18:52:41.350493498Z","actor":"Prad Nukala","issue_id":"chezmoi-utl","extra":{"field":"status","new_value":"in_progress","old_value":"open"}} diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 6f053fa..577e0f3 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -13,7 +13,7 @@ {"_type":"issue","id":"chezmoi-fde","title":"starship: prompt config, universal","description":"Keep starship (user decision — omp dropped). dot_config/starship.toml from live; starship in packages.yaml both OSes. Noctalia palette block is auto-generated on linux — noctalia rewrites it in place; acceptable drift, noted.","status":"closed","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:06Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:51:26Z","closed_at":"2026-08-09T18:51:26Z","close_reason":"starship.toml added from live; packages updated","dependencies":[{"issue_id":"chezmoi-fde","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:52Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-fde","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:51Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} {"_type":"issue","id":"chezmoi-x7f","title":"lazygit: static config","description":"~/.config/lazygit. Already partially in repo — reconcile.","status":"open","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:06Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:42:06Z","dependencies":[{"issue_id":"chezmoi-x7f","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:51Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-x7f","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:50Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} {"_type":"issue","id":"chezmoi-hou","title":"yazi: config, opener rules OS-branched","description":"~/.config/yazi. open vs xdg-open template branch.","status":"open","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:05Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:42:05Z","dependencies":[{"issue_id":"chezmoi-hou","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:50Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-hou","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:50Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} -{"_type":"issue","id":"chezmoi-sd9","title":"nvim: AstroNvim config, static","description":"chezmoi add ~/.config/nvim as-is, no templating. Exclude lazy-lock churn decision noted.","status":"in_progress","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:05Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:51:54Z","started_at":"2026-08-09T18:51:54Z","dependencies":[{"issue_id":"chezmoi-sd9","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:49Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-sd9","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:49Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} -{"_type":"issue","id":"chezmoi-utl","title":"kitty: live config, OS-branch fonts/paths if needed","description":"~/.config/kitty. Keybinds shared.","status":"open","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:05Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:42:05Z","dependencies":[{"issue_id":"chezmoi-utl","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:50Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-utl","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:50Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} +{"_type":"issue","id":"chezmoi-sd9","title":"nvim: AstroNvim config, static","description":"chezmoi add ~/.config/nvim as-is, no templating. Exclude lazy-lock churn decision noted.","status":"closed","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:05Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:52:20Z","started_at":"2026-08-09T18:51:54Z","closed_at":"2026-08-09T18:52:20Z","close_reason":"Closed","dependencies":[{"issue_id":"chezmoi-sd9","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:49Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-sd9","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:49Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} +{"_type":"issue","id":"chezmoi-utl","title":"kitty: live config, OS-branch fonts/paths if needed","description":"~/.config/kitty. Keybinds shared.","status":"in_progress","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:05Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:52:41Z","started_at":"2026-08-09T18:52:41Z","dependencies":[{"issue_id":"chezmoi-utl","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:50Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-utl","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:50Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} {"_type":"issue","id":"chezmoi-uw2","title":"fish: config, conf.d, functions, completions, bindings","description":"Live ~/.config/fish (Rogz storage superset; Noctalia canonical, deltas to MIGRATION-NOTES). OS-branch clipboard/open/package paths. secrets.fish excluded (own issue). fish -n all files.","status":"closed","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:05Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:51:54Z","started_at":"2026-08-09T18:49:28Z","closed_at":"2026-08-09T18:51:54Z","close_reason":"Closed","dependencies":[{"issue_id":"chezmoi-uw2","depends_on_id":"chezmoi-1a0","type":"blocks","created_at":"2026-08-09T14:42:24Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-uw2","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:49Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-uw2","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:49Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":3,"dependent_count":2,"comment_count":0} -{"_type":"issue","id":"chezmoi-xi5","title":"tmux: config from live, strip vertical geometry","description":"~/.config/tmux (tmux.conf symlink from Vertical-Rogz_AppleStudio). Verify live, gate vertical-display-specific bits if present. Plugins dir decision.","status":"open","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:05Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:42:05Z","dependencies":[{"issue_id":"chezmoi-xi5","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:50Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-xi5","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:50Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} +{"_type":"issue","id":"chezmoi-xi5","title":"tmux: config from live, strip vertical geometry","description":"~/.config/tmux (tmux.conf symlink from Vertical-Rogz_AppleStudio). Verify live, gate vertical-display-specific bits if present. Plugins dir decision.","status":"closed","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:05Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:52:41Z","started_at":"2026-08-09T18:52:20Z","closed_at":"2026-08-09T18:52:41Z","close_reason":"tmux.conf real content (was symlink_); no vertical geometry present; plugins unmanaged, tpm bootstrap in post script","dependencies":[{"issue_id":"chezmoi-xi5","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:50Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-xi5","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:50Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} diff --git a/dot_config/gh/private_config.yml b/dot_config/gh/private_config.yml new file mode 100644 index 0000000..8b25011 --- /dev/null +++ b/dot_config/gh/private_config.yml @@ -0,0 +1,27 @@ +# The current version of the config schema +version: 1 +# What protocol to use when performing git operations. Supported values: ssh, https +git_protocol: https +# What editor gh should run when creating issues, pull requests, etc. If blank, will refer to environment. +editor: +# When to interactively prompt. This is a global config that cannot be overridden by hostname. Supported values: enabled, disabled +prompt: enabled +# Preference for editor-based interactive prompting. This is a global config that cannot be overridden by hostname. Supported values: enabled, disabled +prefer_editor_prompt: disabled +# A pager program to send command output to, e.g. "less". If blank, will refer to environment. Set the value to "cat" to disable the pager. +pager: +# Aliases allow you to create nicknames for gh commands +aliases: + co: pr checkout +# The path to a unix socket through which to send HTTP connections. If blank, HTTP traffic will be handled by net/http.DefaultTransport. +http_unix_socket: +# What web browser gh should use when opening URLs. If blank, will refer to environment. +browser: +# Whether to display labels using their RGB hex color codes in terminals that support truecolor. Supported values: enabled, disabled +color_labels: disabled +# Whether customizable, 4-bit accessible colors should be used. Supported values: enabled, disabled +accessible_colors: disabled +# Whether an accessible prompter should be used. Supported values: enabled, disabled +accessible_prompter: disabled +# Whether to use a animated spinner as a progress indicator. If disabled, a textual progress indicator is used instead. Supported values: enabled, disabled +spinner: enabled diff --git a/dot_config/kitty/goliath.conf b/dot_config/kitty/goliath.conf new file mode 100644 index 0000000..3358fce --- /dev/null +++ b/dot_config/kitty/goliath.conf @@ -0,0 +1,28 @@ +include ssh.conf +listen_on unix:@kitty-goliath +# open straight into goliath's tmux +shell ssh -t 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 diff --git a/dot_config/kitty/kitty.conf b/dot_config/kitty/kitty.conf new file mode 100644 index 0000000..01b0bf3 --- /dev/null +++ b/dot_config/kitty/kitty.conf @@ -0,0 +1,90 @@ +# ── base.conf · shared ── +font_size 13.5 +font_family VictorMono Nerd Font +bold_italic_font auto +window_padding_width 8 +hide_window_decorations yes +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 +cursor_trail 10 +cursor_trail_start_threshold 0 +cursor_trail_decay 0.01 0.05 +cursor_blink_interval 0 + +# real socket is @kitty-local-; scripts must use $KITTY_LISTEN_ON, not this name +listen_on unix:@kitty-local +enabled_layouts splits:split_axis=vertical + +# 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=35 + +# panes = kitty windows +map super+t new_tab +map super+enter new_tab_with_cwd +map super+n new_os_window +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_mn yazi +map ctrl+shift+e launch_tb nvim +map ctrl+shift+a launch_vs omp + +# 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 + +# 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 + +tab_title_template " {index}" +active_tab_title_template " {title}" + + + +# BEGIN_KITTY_FONTS +font_family family="VictorMono Nerd Font Mono" +bold_font auto +italic_font auto +bold_italic_font auto +# END_KITTY_FONTS +include themes/noctalia.conf diff --git a/dot_config/kitty/navigate_kitty.py b/dot_config/kitty/navigate_kitty.py new file mode 100644 index 0000000..63f0f66 --- /dev/null +++ b/dot_config/kitty/navigate_kitty.py @@ -0,0 +1,16 @@ +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 new file mode 100644 index 0000000..38c1261 --- /dev/null +++ b/dot_config/kitty/pass_keys.py @@ -0,0 +1,55 @@ +import re + +from kittens.tui.handler import result_handler +from kitty.key_encoding import KeyEvent, parse_shortcut + +PASSTHROUGH = r'\b(vim|nvim|fzf)\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 new file mode 100644 index 0000000..561af79 --- /dev/null +++ b/dot_config/kitty/select_tab.py @@ -0,0 +1,628 @@ +#!/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 new file mode 100644 index 0000000..03e8660 --- /dev/null +++ b/dot_config/kitty/ssh.conf @@ -0,0 +1,5 @@ +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 new file mode 100644 index 0000000..3b2ddd3 --- /dev/null +++ b/dot_config/kitty/switchtab.py @@ -0,0 +1,103 @@ +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 new file mode 100644 index 0000000..083adc2 --- /dev/null +++ b/dot_config/kitty/symlink_current-theme.conf @@ -0,0 +1 @@ +themes/noctalia.conf diff --git a/dot_config/kitty/themes/koda-dark.conf b/dot_config/kitty/themes/koda-dark.conf new file mode 100644 index 0000000..da9d6ef --- /dev/null +++ b/dot_config/kitty/themes/koda-dark.conf @@ -0,0 +1,74 @@ +# 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 diff --git a/dot_config/kitty/themes/koda-light.conf b/dot_config/kitty/themes/koda-light.conf new file mode 100644 index 0000000..8bcc170 --- /dev/null +++ b/dot_config/kitty/themes/koda-light.conf @@ -0,0 +1,72 @@ +# 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 diff --git a/dot_config/kitty/themes/noctalia.conf b/dot_config/kitty/themes/noctalia.conf new file mode 100644 index 0000000..ccc2a8d --- /dev/null +++ b/dot_config/kitty/themes/noctalia.conf @@ -0,0 +1,32 @@ +color0 #371b36 +color1 #fd4663 +color2 #ef5ce9 +color3 #ed4592 +color4 #ed4c45 +color5 #f48bef +color6 #f48bbb +color7 #f3f2f3 +color8 #716270 +color9 #fd4663 +color10 #ef5ce9 +color11 #ed4592 +color12 #ed4c45 +color13 #f48bef +color14 #f48bbb +color15 #f3f2f3 + +cursor #f3f2f3 +cursor_text_color #291428 +background #291428 +foreground #f3f2f3 +selection_foreground #b6afb6 +selection_background #371b36 +active_border_color #ef5ce9 +inactive_border_color #371b36 +url_color #ef5ce9 + +active_tab_foreground #251824 +active_tab_background #ef5ce9 +inactive_tab_foreground #b6afb6 +inactive_tab_background #371b36 +cursor_trail_color #b6afb6 diff --git a/dot_config/kitty/window_titler.py b/dot_config/kitty/window_titler.py new file mode 100644 index 0000000..4aa5169 --- /dev/null +++ b/dot_config/kitty/window_titler.py @@ -0,0 +1,97 @@ +# ~/.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 diff --git a/dot_config/lazydocker/empty_config.yml b/dot_config/lazydocker/empty_config.yml new file mode 100644 index 0000000..e69de29 diff --git a/dot_config/lazygit/config.yml b/dot_config/lazygit/config.yml index 7e4cbc4..a97afdf 100644 --- a/dot_config/lazygit/config.yml +++ b/dot_config/lazygit/config.yml @@ -439,7 +439,7 @@ keybinding: commitMenu: customCommands: - key: "" - command: "ltui" + command: "gitv {{.Repository.Owner}}/{{.Repository.Name}}" context: "global" output: terminal - key: "" @@ -468,7 +468,7 @@ customCommands: description: "Smart commit with AI" context: "files" command: "gh commit" - output: log + output: terminal - key: "o" description: "start ai pull request" command: "gh pr create --fill -w" diff --git a/dot_config/tmux/symlink_tmux.conf b/dot_config/tmux/symlink_tmux.conf deleted file mode 100644 index cdb4f17..0000000 --- a/dot_config/tmux/symlink_tmux.conf +++ /dev/null @@ -1 +0,0 @@ -/home/prad/.config/dotstate/storage/Vertical-Rogz_AppleStudio/.config/tmux/tmux.conf diff --git a/dot_config/tmux/tmux.conf b/dot_config/tmux/tmux.conf new file mode 100644 index 0000000..0dfdd8e --- /dev/null +++ b/dot_config/tmux/tmux.conf @@ -0,0 +1,135 @@ +# ── Plugins ────────────────────────────────────────────────────────────── +set -g @plugin 'tmux-plugins/tpm' +set -g @plugin 'tmux-plugins/tmux-sensible' +set -g @plugin 'vaaleyard/tmux-dotbar' +set -g @plugin 'tmux-plugins/tmux-resurrect' +set -g @plugin 'joshmedeski/tmux-nerd-font-window-name' +set -g @plugin 'omerxx/tmux-sessionx' +set -g @plugin 'tmux-plugins/tmux-continuum' # keep last +set -g @plugin 'eduwass/tmux-palette' + +# ── Kitty compatibility ────────────────────────────────────────────────── +set-option -g renumber-windows on +set -g base-index 1 +setw -g pane-base-index 1 +set -g default-terminal "tmux-256color" +set -ga terminal-overrides ",xterm-kitty:Tc" +set -as terminal-features ",xterm-kitty:RGB:usstyle:hyperlinks:clipboard:extkeys" +set -g extended-keys always +set -gq extended-keys-format csi-u +set-option -g status-position top + +# ── Status bar ───────────────────────────────────────────────────────────────── +# Dotbar +set -g @tmux-dotbar-position top +set -g @tmux-dotbar-session-position "right" +set -g @tmux-dotbar-rounded true +set -g @tmux-dotbar-bg "#0B0E14" +set -g @tmux-dotbar-fg "#475266" +set -g @tmux-dotbar-fg-current "#BFBDB6" +set -g @tmux-dotbar-fg-session "#565B66" +set -g @tmux-dotbar-fg-prefix "#95E6CB" +set -g @tmux-dotbar-justify "absolute-centre" +set -g @tmux-dotbar-left "true" +set -g @tmux-dotbar-right "false" +set -g @tmux-dotbar-session-position "left" +set -g @tmux-dotbar-session-text "  " +set -g @tmux-dotbar-status-right-text " %H:%M " +set -g @tmux-dotbar-window-status-format " #W " +set -g @tmux-dotbar-window-status-separator " • " +set -g @tmux-dotbar-maximized-icon "󰊓" +set -g @tmux-dotbar-show-maximized-icon-for-all-tabs false +set -g @tmux-dotbar-bold-status false +set -g @tmux-dotbar-bold-current-window false +set -g @tmux-dotbar-ssh-enabled true +set -g @tmux-dotbar-ssh-icon '󰌘' +set -g @tmux-dotbar-ssh-icon-only false + +# Styling +set -g status-style fg=default,bg=default +set -g popup-style 'bg=default' +set -g popup-border-style 'bg=default,fg=#5c5c66' +set -g popup-border-lines rounded +set -g @sessionx-additional-options "--color 'bg:-1,bg+:-1,gutter:-1,border:-1,preview-bg:-1'" + +# Kitty graphics protocol passthrough (yazi / image preview) +set -g allow-passthrough on +set -ga update-environment TERM +set -ga update-environment TERM_PROGRAM + +# ── Clipboard (Wayland / Niri) ─────────────────────────────────────────── +set -g set-clipboard on # OSC 52 — covers ssh'd sessions +set -g mouse on +setw -g mode-keys vi +bind -T copy-mode-vi v send -X begin-selection +bind -T copy-mode-vi y send -X copy-pipe-and-cancel "wl-copy" +bind -T copy-mode-vi Y send -X copy-pipe-and-cancel "wl-copy" \; send -X cancel +bind -T copy-mode-vi MouseDragEnd1Pane send -X copy-pipe-and-cancel "wl-copy" + +# ── Sessions ───────────────────────────────────────────────────────────── + +set -g @resurrect-strategy-nvim 'session' +set -g @resurrect-capture-pane-contents 'on' +set -g @resurrect-processes 'ssh psql mysql sqlite3 purple lazygit lazydocker "claude->claude --continue" opencode' +set -g @continuum-save-interval '10' +set -g @continuum-restore 'off' # restore is owned by kitty-tmux launcher +set -g @sessionx-bind 'o' +set -g @sessionx-zoxide-mode 'on' + +# ── Titles → Niri active window ────────────────────────────────────────── + +set-option -g set-titles on +set-option -g set-titles-string "#T (#S)" +set-option -g status-interval 1 +set-window-option -g automatic-rename on + +# ── QoL ────────────────────────────────────────────────────────────────── +set -g history-limit 50000 +set -g base-index 1 +setw -g pane-base-index 1 +set -g renumber-windows on + +# ── Keybinds ───────────────────────────────────────────────────────────── +bind -n S-Enter send-keys Escape '[13;2u' +# New window: on @ssh_host sessions (set by the Vicinae tmux-sessions +# extension) connect to the same remote; otherwise inherit the local cwd. +# The remote host is read at runtime — tmux does NOT format-expand a +# new-window shell-command, so "#{@ssh_host}" there would be taken literally. +bind-key t if-shell -F '#{@ssh_host}' \ + 'new-window "ssh $(tmux show-options -v @ssh_host)"' \ + 'new-window -c "#{pane_current_path}"' +bind-key [ previous-window +bind-key ] next-window +bind-key H previous-window +bind-key L next-window +set -g @palette-key 'C-Space' # optional, default: C-Space (no-prefix) +set -g @palette-find-pane-key 'M-f' # optional, no binding by default +set -g @palette-move-pane-key 'M-m' # optional, no binding by default + + +# Splits inherit cwd (kitty super+t / super+n land here) +# vsplit: new pane right at 35% width · hsplit: new pane below at 25% height +bind % split-window -h -l 30% -c "#{pane_current_path}" +bind '"' split-window -v -l 25% -c "#{pane_current_path}" + +# Copy-mode entry (default prefix+[ is rebound above) +bind Escape copy-mode + +# ── Pane navigation: ctrl+hjkl, no prefix ──────────────────────────────── +# Vim-aware: if the pane runs (n)vim, forward the key so vim handles its +# own window movement (pair with vim-tmux-navigator in AstroNvim). +is_vim="ps -o state= -o comm= -t '#{pane_tty}' | grep -iqE '^[^TXZ ]+ +(\\S+\\/)?g?(view|l?n?vim?x?|fzf)(diff)?$'" +bind -n C-h if-shell "$is_vim" 'send-keys C-h' 'select-pane -L' +bind -n C-j if-shell "$is_vim" 'send-keys C-j' 'select-pane -D' +bind -n C-k if-shell "$is_vim" 'send-keys C-k' 'select-pane -U' +bind -n C-l if-shell "$is_vim" 'send-keys C-l' 'select-pane -R' +# Same nav from inside copy-mode +bind -T copy-mode-vi C-h select-pane -L +bind -T copy-mode-vi C-j select-pane -D +bind -T copy-mode-vi C-k select-pane -U +bind -T copy-mode-vi C-l select-pane -R +# Clear-screen escape hatch (C-l is consumed above) +bind C-l send-keys C-l + +# Initialize TMUX plugin manager (keep this line at the very bottom of tmux.conf) +run '~/.tmux/plugins/tpm/tpm' diff --git a/dot_config/yazi/init.lua b/dot_config/yazi/init.lua index 1630a69..29cb69a 100644 --- a/dot_config/yazi/init.lua +++ b/dot_config/yazi/init.lua @@ -1 +1,22 @@ require("sshfs"):setup() + +require("bunny"):setup({ + hops = { + { key = "/", path = "/", }, + { key = "t", path = "/tmp", }, + { key = "~", path = "~", desc = "Home" }, + { key = "m", path = "~/Music", desc = "Music" }, + { key = "d", path = "~/Desktop", desc = "Desktop" }, + { key = "D", path = "~/Developer", desc = "Developer" }, + { key = "c", path = "~/.config", desc = "Config files" }, + { key = { "l", "s" }, path = "~/.local/share", desc = "Local share" }, + { key = { "l", "b" }, path = "~/.local/bin", desc = "Local bin" }, + { key = { "l", "t" }, path = "~/.local/state", desc = "Local state" }, + -- key and path attributes are required, desc is optional + }, + desc_strategy = "path", -- If desc isn't present, use "path" or "filename", default is "path" + ephemeral = true, -- Enable ephemeral hops, default is true + tabs = true, -- Enable tab hops, default is true + notify = false, -- Notify after hopping, default is false + fuzzy_cmd = "fzf", -- Fuzzy searching command, default is "fzf" +}) diff --git a/dot_config/yazi/keymap.toml b/dot_config/yazi/keymap.toml index 98e8f05..3052b7f 100644 --- a/dot_config/yazi/keymap.toml +++ b/dot_config/yazi/keymap.toml @@ -8,6 +8,21 @@ on = [ "g", "r" ] run = 'shell -- ya emit cd "$(git rev-parse --show-toplevel)"' desc = "Go to git root" +[[mgr.prepend_keymap]] +on = [ "g", "C" ] +run = 'shell -- ya emit cd "$HOME/.local/share/chezmoi"' +desc = "Go to chezmoi root" + +[[mgr.prepend_keymap]] +desc = "Start bunny.yazi" +on = ";" +run = "plugin bunny" + +# Optionally, add another keymap to immediately fuzzy search bookmarks +[[mgr.prepend_keymap]] +desc = "Start bunny.yazi fuzzy search" +on = "'" +run = "plugin bunny fuzzy" [[mgr.prepend_keymap]] on = [ "g", "d"] diff --git a/dot_config/yazi/package.toml b/dot_config/yazi/package.toml index 24db139..ba18f56 100644 --- a/dot_config/yazi/package.toml +++ b/dot_config/yazi/package.toml @@ -33,5 +33,15 @@ use = "cap153/tv" rev = "b6b1f12" hash = "4bdf0523d3711d20982ad80b061dedf2" +[[plugin.deps]] +use = "stelcodes/bunny" +rev = "71b14a3" +hash = "18a2829738214fe9bca99a2fef19113d" + +[[plugin.deps]] +use = "coder0x6675/osc7" +rev = "ae73046" +hash = "4f73595837992eeefcb525c0a95b92ba" + [flavor] deps = [] diff --git a/dot_config/yazi/theme.toml b/dot_config/yazi/theme.toml index 13649c4..bc5cb70 100644 --- a/dot_config/yazi/theme.toml +++ b/dot_config/yazi/theme.toml @@ -13,12 +13,15 @@ prepend_dirs = [ { name = "wltbs", text = "󱃪" }, { name = "Shared", text = "" }, { name = "Templates", text = "" }, + {name = "env", text = ""}, {name = "go", text = "󰟓"}, {name = "js", text = ""}, {name = "dart", text = ""}, - {name = "python", text = ""}, + {name = "py", text = ""}, + {name = "rs", text = ""}, {name = "tex", text = ""}, {name = "md", text = ""}, + {name = "docs", text = ""}, {name = "prad.codes", text = ""} ] diff --git a/dot_config/yazi/yazi.toml b/dot_config/yazi/yazi.toml.tmpl similarity index 82% rename from dot_config/yazi/yazi.toml rename to dot_config/yazi/yazi.toml.tmpl index 3496617..a38509e 100644 --- a/dot_config/yazi/yazi.toml +++ b/dot_config/yazi/yazi.toml.tmpl @@ -1,3 +1,6 @@ +[mgr] +ratio = [0, 2, 6] + [opener] play = [ { run = "mpv %s", orphan = true, for = "unix" }, @@ -6,7 +9,7 @@ edit = [ { run = "nvim %s", block = true, for = "unix" }, ] open = [ - { run = "xdg-open %s1", desc = "Open" }, + { run = "{{ if eq .chezmoi.os "darwin" }}open{{ else }}xdg-open{{ end }} %s1", desc = "Open" }, ] [open]