From 9d347648153f1076ccaaf3380b3596b2ca1f578f Mon Sep 17 00:00:00 2001 From: rootiest Date: Sat, 30 May 2026 23:40:53 -0400 Subject: [PATCH] feat(watcher): add scrollback history logging on window/pane close MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces watcher.py — a kitty Python watcher that captures terminal scrollback to ~/.terminal_history/ whenever any window or pane is closed via Ctrl+D, keybind, or full kitty quit. Works alongside the fish smart_exit function, which handles graceful shell exits and sets a user var flag to prevent duplicate logs. Fixes the kitty.conf watcher directive (removed invalid --type=global prefix that caused a FileNotFoundError on every kitty startup). --- README.md | 12 ++++++++ kitty.conf | 4 +++ watcher.py | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 watcher.py diff --git a/README.md b/README.md index ac455dd..77682a9 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ The rootiest kitty configuration you will ever see! - Smart key bindings with NeoVim-aware passthrough - Integration with NeoVim (kitty-scrollback.nvim for in-editor scrollback) - Integration with fish shell +- Automatic scrollback history logging on every window/pane close - Catppuccin Mocha theme with 85% background opacity and blur - Cursor trail effect for enhanced visibility - Highly-tuned Iosevka Rootiest v2 font with custom OpenType features @@ -81,6 +82,17 @@ The rootiest kitty configuration you will ever see! - Session saving (F11) and config reload (F12) - Integrates cleanly with other Rootiest projects +## Configuration + +### Scrollback History + +Terminal scrollback is automatically saved to `~/.terminal_history/` whenever a window or pane is closed (including `Ctrl+D`, closing a pane via keybind, or quitting kitty). The fish `smart_exit` function handles graceful shell exits; `watcher.py` catches all other close events. + +| Variable | Default | Description | +|---|---|---| +| `SCROLLBACK_HISTORY_DIR` | `~/.terminal_history` | Directory where scrollback logs are written | +| `SCROLLBACK_HISTORY_MAX_FILES` | `100` | Maximum number of log files to retain | + ## Companion Tools [Rootiest Fish](https://github.com/rootiest/rootiest-fish) - diff --git a/kitty.conf b/kitty.conf index 98a6358..8dd580a 100644 --- a/kitty.conf +++ b/kitty.conf @@ -11,6 +11,10 @@ notify_on_cmd_finish never copy_on_select yes # }}} +# {{{ Watcher +watcher watcher.py +# }}} + # {{{ Window remember_window_size no initial_window_width 150c diff --git a/watcher.py b/watcher.py new file mode 100644 index 0000000..a76f943 --- /dev/null +++ b/watcher.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +# Copyright (C) 2026 Rootiest +# SPDX-License-Identifier: AGPL-3.0-or-later + +import datetime +import os +import re +from kitty.boss import Boss +from kitty.window import Window + +_logged_windows: set[int] = set() + + +def save_scrollback_safely(window: Window) -> None: + if window.user_vars.get("logged_by_shell") == "true": + return + if window.id in _logged_windows: + return + + home = os.path.expanduser("~") + snapshot_dir = os.environ.get( + "SCROLLBACK_HISTORY_DIR", os.path.join(home, ".terminal_history") + ) + max_files_str = os.environ.get("SCROLLBACK_HISTORY_MAX_FILES", "100") + + try: + max_files = int(max_files_str) + except ValueError: + max_files = 100 + + os.makedirs(snapshot_dir, exist_ok=True) + + # Safety filter: prevent TUI capture if an editor is focused + try: + foreground_processes = window.child.foreground_processes + if foreground_processes: + cmdline = foreground_processes[-1].get("cmdline") or [""] + base_app = os.path.basename(cmdline[0]) + if re.match(r"^(nvim|vim|vi|nano|emacs|tmux)$", base_app): + return + except Exception: + pass + + text = window.as_text(as_ansi=True, add_history=True) + if not text.strip(): + return + + text_cleaned = re.sub(r"^\x1b\[38;2;[0-9;]*m", "", text) + + timestamp = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + filename = os.path.join(snapshot_dir, f"scrollback_{timestamp}.log") + + try: + with open(filename, "w", encoding="utf-8") as f: + f.write(text_cleaned) + except IOError: + return + + _logged_windows.add(window.id) + + try: + files = sorted( + os.path.join(snapshot_dir, f) + for f in os.listdir(snapshot_dir) + if f.startswith("scrollback_") and f.endswith(".log") + ) + for old in files[: max(0, len(files) - max_files)]: + os.remove(old) + except Exception: + pass + + +def on_close(boss: Boss, window: Window, data: dict) -> None: + save_scrollback_safely(window) + + +def on_quit(boss: Boss, window: Window, data: dict) -> None: + if not data.get("confirmed"): + return + for win in boss.all_windows: + save_scrollback_safely(win)