"""Keyboard-driven tree picker used by choose_tab.py.""" import json import re import sys import unicodedata from kittens.tui.handler import Handler, result_handler from kittens.tui.loop import Loop from kitty.fast_data_types import truncate_point_for_length, wcswidth RESET = '\x1b[0m' CONTROLS = re.compile( r'\x1b(?:\][^\x07\x1b]*(?:\x07|\x1b\\)|[PX^_].*?\x1b\\|' r'\[[0-?]*[ -/]*[@-~]|[@-_])', re.DOTALL) SGR = re.compile(r'\x1b\[[0-9;:]*m\Z') def clean_text(text): text = CONTROLS.sub('', text) return ''.join(c for c in text if not unicodedata.category(c).startswith('C')) def clip_text(text, width): end = truncate_point_for_length(text, width) while end < len(text) and unicodedata.category(text[end]) in ('Mn', 'Me'): end += 1 return text[:end] def clip_ansi(text, width): """Retain only SGR formatting; never let a snapshot control the terminal.""" result = [] used = 0 end = 0 for match in CONTROLS.finditer(text): plain = clean_text(text[end:match.start()]) piece = clip_text(plain, max(0, width - used)) result.append(piece) used += wcswidth(piece) if len(piece) != len(plain): return ''.join(result) + RESET if SGR.fullmatch(match.group()): result.append(match.group()) end = match.end() plain = clean_text(text[end:]) result.append(clip_text(plain, max(0, width - used))) return ''.join(result) + RESET class Preview: """Position a colored, cell-clipped preview inside a terminal rectangle.""" def __init__(self, top, width, height): self.top, self.width, self.height = top, width, height self.output = [] def put(self, x, y, text, width): if 0 <= x < self.width and 0 <= y < self.height: width = min(width, self.width - x) if width > 0: self.output.append(f'\x1b[{self.top + y};{x + 1}H{RESET}' + clip_ansi(text, width)) def box(self, x, y, width, height, title='', active=False): if width < 2 or height < 2: return color = '\x1b[33m' if active else '\x1b[90m' self.put(x, y, color + '┌' + '─' * (width - 2) + '┐', width) for row in range(y + 1, y + height - 1): self.put(x, row, color + '│', 1) self.put(x + width - 1, row, color + '│', 1) self.put(x, y + height - 1, color + '└' + '─' * (width - 2) + '┘', width) if title: self.put(x + 1, y, color + ' ' + clean_text(title) + ' ', width - 2) def badge(self, x, y, width, height, label, active): if width < 5 or height < 3: return label = clip_text(clean_text(label), width - 4) badge_width = wcswidth(label) + 4 bx = x + (width - badge_width) // 2 by = y + (height - 3) // 2 self.box(bx, by, badge_width, 3) color = '\x1b[33m' if active else '\x1b[34m' self.put(bx + 1, by + 1, color + ' ' + label + ' ', badge_width - 2) def tab(self, snapshot, x, y, width, height, title=None, active=False): panes = snapshot.get('panes', []) if snapshot else [] if not panes: self.put(x, y, 'Preview unavailable', width) if title is not None: self.badge(x, y, width, height, title, active) return left = min(p['rect'][0] for p in panes) top = min(p['rect'][1] for p in panes) right = max(p['rect'][2] for p in panes) bottom = max(p['rect'][3] for p in panes) small = 0 dividers = [] badges = [] for index, pane in enumerate(panes, 1): l, t, r, b = pane['rect'] px = x + round((l - left) * width / max(1, right - left)) py = y + round((t - top) * height / max(1, bottom - top)) pw = x + round((r - left) * width / max(1, right - left)) - px ph = y + round((b - top) * height / max(1, bottom - top)) - py if l > left: dividers.extend((px, row, '│', 1) for row in range(py, py + ph)) px, pw = px + 1, pw - 1 if t > top: dividers.append((px, py, '─' * max(0, pw), pw)) py, ph = py + 1, ph - 1 if pw < 1 or ph < 1: small += 1 continue lines = pane.get('lines') for row, line in enumerate((lines if lines is not None else ['Preview unavailable'])[:ph]): self.put(px, py + row, line, pw) if title is None: badges.append((px, py, pw, ph, str(index), pane['active'])) for dx, dy, text, count in dividers: self.put(dx, dy, '\x1b[90m' + text, count) for badge in badges: self.badge(*badge) if title is not None: self.badge(x, y, width, height, title, active) if small: self.put(x, y + height - 1, f'{small} pane(s) too small', width) class WindowPicker(Handler): def __init__(self, entries, collapsed=False, snapshots=None): self.all_entries = entries self.groups = {i for i, entry in enumerate(entries) if entry['kind'] == 'window'} self.collapsed = set(self.groups) if collapsed else set() self.selected = 0 self.offset = 0 self.answer = None self.query = '' self.searching = False self.search_origin = ('', 0) self.snapshots = snapshots or {} self.preview_pages = {} def visible_indices(self): hidden = False visible = [] for index in range(len(self.all_entries)): if index in self.groups: hidden = index in self.collapsed visible.append(index) elif not hidden: visible.append(index) return visible @property def entries(self): return [self.all_entries[i] for i in self.visible_indices()] def expand_group(self, expand): visible = self.visible_indices() if not visible: return index = visible[self.selected] if index not in self.groups: return if expand: self.collapsed.discard(index) else: self.collapsed.add(index) self.draw() def matches(self): if not self.query: return [] query = self.query.casefold() return [i for i, entry in enumerate(self.entries) if query in clean_text(entry['label']).casefold()] def update_search(self): matches = self.matches() if matches and self.selected not in matches: self.selected = next((i for i in matches if i >= self.selected), matches[0]) self.draw() def next_match(self, delta): matches = self.matches() if matches: ordered = matches if delta > 0 else list(reversed(matches)) self.selected = next( (i for i in ordered if (i - self.selected) * delta > 0), ordered[0]) self.draw() def initialize(self): self.draw() def preview_height(self): if self.screen_size.rows < 18 or self.screen_size.cols < 24: return 0 return (self.screen_size.rows - 1) // 2 def page_preview(self, delta): if self.entries and self.preview_height(): entry = self.entries[self.selected] if entry['kind'] == 'window': capacity = max(1, (self.screen_size.cols - 2) // 24) count = len(entry['tabs']) default = entry['tabs'].index(entry['tab_id']) // capacity page = self.preview_pages.get(id(entry), default) self.preview_pages[id(entry)] = max(0, min((count - 1) // capacity, page + delta)) self.draw() def draw_preview(self, height): preview = Preview(self.screen_size.rows - height, self.screen_size.cols, height) if not self.entries: preview.box(0, 0, preview.width, height, 'Snapshot') return ''.join(preview.output) entry = self.entries[self.selected] tab_ids = entry.get('tabs', [entry['tab_id']]) capacity = max(1, (preview.width - 2) // 24) if entry['kind'] == 'window' else 1 pages = max(1, (len(tab_ids) + capacity - 1) // capacity) default = tab_ids.index(entry['tab_id']) // capacity page = min(pages - 1, self.preview_pages.get(id(entry), default)) self.preview_pages[id(entry)] = page start = page * capacity shown = tab_ids[start:start + capacity] label = f'{entry["title"]} — snapshot' if pages > 1: label += f' — tabs {start + 1}–{start + len(shown)}/{len(tab_ids)} [{page + 1}/{pages}]' preview.box(0, 0, preview.width, height, label) inner_width = preview.width - 2 for offset, tab_id in enumerate(shown): left = 1 + offset * inner_width // len(shown) right = 1 + (offset + 1) * inner_width // len(shown) if offset: for row in range(1, height - 1): preview.put(left, row, '\x1b[90m│', 1) left += 1 snapshot = self.snapshots.get(str(tab_id)) title = None if entry['kind'] == 'window': title = f'{start + offset + 1}:{snapshot["title"] if snapshot else "Tab"}' active = tab_id == entry['tab_id'] preview.tab(snapshot, left, 1, right - left, height - 2, title, active) return ''.join(preview.output) def draw(self): height = self.preview_height() rows = max(1, self.screen_size.rows - 1 - height) cols = max(1, self.screen_size.cols) self.offset = min(self.offset, self.selected) self.offset = max(self.offset, self.selected - rows + 1) output = ["\x1b[?25l\x1b[H\x1b[2J"] matches = self.matches() visible = self.visible_indices() for index in range(self.offset, min(len(self.entries), self.offset + rows)): label = self.entries[index]['label'] if visible[index] in self.collapsed: label = '+' + label[1:] # Titles are untrusted terminal text: strip control sequences/characters. label = clean_text(label) line = f"({index}) {label}" line = line[:truncate_point_for_length(line, cols)] style = "\x1b[1;33m" if index in matches else "" if index == self.selected: style += "\x1b[7m" output.append(f"{style}{line}\x1b[K\x1b[0m\r\n") if height: output.append(self.draw_preview(height)) footer = 'j/k ↑/↓ move h/l ←/→ fold / search' footer += ' [] page' if height else '' footer += ' Enter select Esc cancel' if not height: footer += ' Preview needs 24 cols × 18 rows' if self.searching or self.query: position = str(matches.index(self.selected) + 1) if self.selected in matches else '-' status = f"{position}/{len(matches)}" if matches else "no matches" help_text = "Enter: finish search Esc: undo" if self.searching else "n/N: next/previous /: search" footer = f"/{self.query} [{status}] {help_text}" footer = footer[:truncate_point_for_length(footer, cols)] output.append(f"\x1b[0m\x1b[{self.screen_size.rows};1H{footer}") self.write(''.join(output)) def move(self, delta): self.selected = max(0, min(len(self.entries) - 1, self.selected + delta)) self.draw() def on_text(self, text, in_bracketed_paste=False): if len(text) > 1 and not in_bracketed_paste: for char in text: self.on_text(char) return if self.searching: self.query += ''.join(c for c in text if not unicodedata.category(c).startswith('C')) self.update_search() elif not in_bracketed_paste: if text == '/': self.search_origin = (self.query, self.selected) self.query = '' self.searching = True self.draw() elif text == 'n': self.next_match(1) elif text == 'N': self.next_match(-1) elif text == 'j': self.move(1) elif text == 'k': self.move(-1) elif text == 'h': self.expand_group(False) elif text == 'l': self.expand_group(True) elif text == '[': self.page_preview(-1) elif text == ']': self.page_preview(1) elif text == 'q': self.quit_loop(0) def on_key(self, key): if key.is_release: return if self.searching: if key.matches('enter'): self.searching = False self.draw() return if key.matches('escape'): self.query, self.selected = self.search_origin self.searching = False self.draw() return if key.matches('backspace'): self.query = self.query[:-1] self.update_search() return if key.matches('ctrl+u'): self.query = '' self.update_search() return if key.matches('left') or key.matches('right'): return if key.matches('down'): self.move(1) elif key.matches('up'): self.move(-1) elif key.matches('left'): self.expand_group(False) elif key.matches('right'): self.expand_group(True) elif key.matches('enter'): if self.entries: self.answer = self.entries[self.selected]['tab_id'] self.quit_loop(0) elif key.matches('escape'): self.quit_loop(0) def on_resize(self, size): super().on_resize(size) self.draw() def on_interrupt(self): self.quit_loop(0) def on_eot(self): self.quit_loop(0) def main(args): payload = json.load(sys.stdin) picker = WindowPicker(payload['entries'], collapsed='--collapsed' in args[1:], snapshots=payload['snapshots']) Loop().loop(picker) return picker.answer @result_handler() def handle_result(args, answer, target_window_id, boss): # choose_tab.py supplies the callback and activates after this overlay closes. pass