mirror of
https://github.com/prdlk/dotfiles.git
synced 2026-11-02 19:32:43 +00:00
config(kitty,yazi,lazygit,lazydocker,gh): add universal tui tools from live home
kitty full config (theme symlink preserved, VictorMono NF). yazi opener OS-branched via template (open vs xdg-open). yazi/lazygit refreshed to live. lazydocker default empty config. gh config.yml as private_ — hosts.yml excluded (oauth token). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f30802426d
commit
3c691e8dc5
24 files changed
+1426
-8
No files matched your search
@@ -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
|
||||
@@ -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-<PID>; 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
|
||||
@@ -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])
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -0,0 +1,5 @@
|
||||
hostname goliath
|
||||
share_connections yes
|
||||
# optional niceties:
|
||||
# copy .config/nvim .config/tmux
|
||||
# interpreter /usr/bin/fish
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
themes/noctalia.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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user