feat(watcher): add scrollback history logging on window/pane close #2
@@ -73,6 +73,7 @@ The rootiest kitty configuration you will ever see!
|
|||||||
- Smart key bindings with NeoVim-aware passthrough
|
- Smart key bindings with NeoVim-aware passthrough
|
||||||
- Integration with NeoVim (kitty-scrollback.nvim for in-editor scrollback)
|
- Integration with NeoVim (kitty-scrollback.nvim for in-editor scrollback)
|
||||||
- Integration with fish shell
|
- Integration with fish shell
|
||||||
|
- Automatic scrollback history logging on every window/pane close
|
||||||
- Catppuccin Mocha theme with 85% background opacity and blur
|
- Catppuccin Mocha theme with 85% background opacity and blur
|
||||||
- Cursor trail effect for enhanced visibility
|
- Cursor trail effect for enhanced visibility
|
||||||
- Highly-tuned Iosevka Rootiest v2 font with custom OpenType features
|
- 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)
|
- Session saving (F11) and config reload (F12)
|
||||||
- Integrates cleanly with other Rootiest projects
|
- 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
|
## Companion Tools
|
||||||
|
|
||||||
[Rootiest Fish](https://github.com/rootiest/rootiest-fish) -
|
[Rootiest Fish](https://github.com/rootiest/rootiest-fish) -
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ notify_on_cmd_finish never
|
|||||||
copy_on_select yes
|
copy_on_select yes
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
# {{{ Watcher
|
||||||
|
watcher watcher.py
|
||||||
|
# }}}
|
||||||
|
|
||||||
# {{{ Window
|
# {{{ Window
|
||||||
remember_window_size no
|
remember_window_size no
|
||||||
initial_window_width 150c
|
initial_window_width 150c
|
||||||
|
|||||||
+81
@@ -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)
|
||||||
Reference in New Issue
Block a user