From c2ec031af25e9ef37ff0edd9acca3a1cd6323d54 Mon Sep 17 00:00:00 2001 From: rootiest Date: Tue, 16 Jun 2026 02:07:23 -0400 Subject: [PATCH 1/6] feat(kitty): add canonical version-marked scrollback watcher --- kitty/fish-config-watcher.py | 90 ++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 kitty/fish-config-watcher.py diff --git a/kitty/fish-config-watcher.py b/kitty/fish-config-watcher.py new file mode 100644 index 0000000..4796b94 --- /dev/null +++ b/kitty/fish-config-watcher.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +# Copyright (C) 2026 Rootiest +# SPDX-License-Identifier: AGPL-3.0-or-later +# fish-config-watcher-version: 1 +# +# Managed by fish-config `kitty-logging`. Do not edit in place — edit the +# canonical copy at ~/.config/fish/kitty/fish-config-watcher.py and re-run +# `kitty-logging install`. + +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: + home = os.path.expanduser("~") + xdg_config = os.environ.get("XDG_CONFIG_HOME", os.path.join(home, ".config")) + sentinel = os.path.join(xdg_config, "fish", ".logging_disabled") + if os.path.exists(sentinel): + return + if window.user_vars.get("logged_by_shell") == "true": + return + if window.id in _logged_windows: + return + + 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) -- 2.52.0 From ca0a35e8c7d49fbb5ccc7f6f92b7ae98c092d939 Mon Sep 17 00:00:00 2001 From: rootiest Date: Tue, 16 Jun 2026 02:09:43 -0400 Subject: [PATCH 2/6] feat(kitty): add kitty-logging resolver/inspection helpers --- functions/__kitty_logging_dir.fish | 25 ++++++++++++++++++ functions/__kitty_logging_has_watcher.fish | 22 ++++++++++++++++ functions/__kitty_logging_version.fish | 30 ++++++++++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 functions/__kitty_logging_dir.fish create mode 100644 functions/__kitty_logging_has_watcher.fish create mode 100644 functions/__kitty_logging_version.fish diff --git a/functions/__kitty_logging_dir.fish b/functions/__kitty_logging_dir.fish new file mode 100644 index 0000000..adc6b5b --- /dev/null +++ b/functions/__kitty_logging_dir.fish @@ -0,0 +1,25 @@ +# Copyright (C) 2026 Rootiest +# SPDX-License-Identifier: AGPL-3.0-or-later + +# SYNOPSIS +# __kitty_logging_dir +# +# DESCRIPTION +# Echoes the Kitty configuration directory, mirroring Kitty's own resolution +# order: $KITTY_CONFIG_DIRECTORY, else $XDG_CONFIG_HOME/kitty, else +# ~/.config/kitty. +# +# RETURNS +# 0 Always (path printed to stdout) +# +# EXAMPLE +# set -l dir (__kitty_logging_dir) +function __kitty_logging_dir --description 'Resolve the Kitty config directory' + if set -q KITTY_CONFIG_DIRECTORY; and test -n "$KITTY_CONFIG_DIRECTORY" + echo $KITTY_CONFIG_DIRECTORY + else if set -q XDG_CONFIG_HOME; and test -n "$XDG_CONFIG_HOME" + echo $XDG_CONFIG_HOME/kitty + else + echo $HOME/.config/kitty + end +end diff --git a/functions/__kitty_logging_has_watcher.fish b/functions/__kitty_logging_has_watcher.fish new file mode 100644 index 0000000..7c4d00f --- /dev/null +++ b/functions/__kitty_logging_has_watcher.fish @@ -0,0 +1,22 @@ +# Copyright (C) 2026 Rootiest +# SPDX-License-Identifier: AGPL-3.0-or-later + +# SYNOPSIS +# __kitty_logging_has_watcher +# +# DESCRIPTION +# Succeeds (returns 0) when the top-level kitty.conf contains an active +# (non-commented) `watcher` directive — whether the fish-config managed one or +# a user's own. Used to suppress the setup reminder and to inform status. +# +# RETURNS +# 0 An active watcher directive is present +# 1 None present, or kitty.conf does not exist +# +# EXAMPLE +# __kitty_logging_has_watcher; and echo "already wired" +function __kitty_logging_has_watcher --description 'True if kitty.conf has an active watcher directive' + set -l conf (__kitty_logging_dir)/kitty.conf + test -f $conf; or return 1 + command grep -qE '^[[:space:]]*watcher[[:space:]]' $conf +end diff --git a/functions/__kitty_logging_version.fish b/functions/__kitty_logging_version.fish new file mode 100644 index 0000000..1911d9e --- /dev/null +++ b/functions/__kitty_logging_version.fish @@ -0,0 +1,30 @@ +# Copyright (C) 2026 Rootiest +# SPDX-License-Identifier: AGPL-3.0-or-later + +# SYNOPSIS +# __kitty_logging_version +# +# DESCRIPTION +# Prints the integer value of the "fish-config-watcher-version:" marker in the +# given watcher file, or 0 if the file is missing or has no marker. +# +# ARGUMENTS +# file Path to a watcher script +# +# RETURNS +# 0 Always (integer printed to stdout) +# +# EXAMPLE +# __kitty_logging_version ~/.config/kitty/fish-config-watcher.py +function __kitty_logging_version --argument-names file --description 'Read the watcher version marker (0 if absent)' + if not test -f "$file" + echo 0 + return 0 + end + set -l v (string match -gr 'fish-config-watcher-version: *([0-9]+)' <$file) + if set -q v[1]; and test -n "$v[1]" + echo $v[1] + else + echo 0 + end +end -- 2.52.0 From 4f266f691bc6fd2a3bcfba25ca52dc19bb269218 Mon Sep 17 00:00:00 2001 From: rootiest Date: Tue, 16 Jun 2026 02:12:47 -0400 Subject: [PATCH 3/6] feat(kitty): add kitty-logging install/uninstall/status/dismiss command --- functions/kitty-logging.fish | 160 +++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 functions/kitty-logging.fish diff --git a/functions/kitty-logging.fish b/functions/kitty-logging.fish new file mode 100644 index 0000000..7747afc --- /dev/null +++ b/functions/kitty-logging.fish @@ -0,0 +1,160 @@ +# Copyright (C) 2026 Rootiest +# SPDX-License-Identifier: AGPL-3.0-or-later + +# SYNOPSIS +# kitty-logging [install | uninstall | status | dismiss] [-h] +# +# DESCRIPTION +# Manages the fish-config Kitty scrollback watcher. `install` copies the +# canonical watcher into the Kitty config dir and wires it into kitty.conf via +# a sentinel-marked managed block (commenting out any conflicting active +# watcher line to avoid double-capture). `uninstall` reverses it. `status` +# reports wiring, watcher version, and C5 logging state. `dismiss` silences the +# per-session setup reminder. Runtime capture remains gated by the C5 +# .logging_disabled sentinel; install affects future Kitty instances only. +# +# ARGUMENTS +# install Copy the watcher and add the managed block to kitty.conf +# uninstall Remove the managed block and the installed watcher +# status Report wiring, watcher version, and C5 logging state +# dismiss Stop the per-session reminder +# -h, --help Show this help +# +# RETURNS +# 0 Success +# 1 Unknown subcommand/flag, kitty missing, or a write failure +# +# EXAMPLE +# kitty-logging install +# kitty-logging status +function kitty-logging --description 'Install/manage the fish-config Kitty scrollback watcher' + set -l c_head (set_color --bold cyan) + set -l c_cmd (set_color --bold white) + set -l c_flag (set_color yellow) + set -l c_ok (set_color green) + set -l c_warn (set_color yellow) + set -l c_err (set_color red) + set -l c_dim (set_color brblack) + set -l c_reset (set_color normal) + + set -l cmd $argv[1] + + if test -z "$cmd"; or contains -- "$cmd" -h --help + echo "$c_head""Usage:$c_reset $c_cmd""kitty-logging$c_reset $c_flag""[install|uninstall|status|dismiss]$c_reset $c_flag""[-h]$c_reset" + echo + echo " Manage the fish-config Kitty scrollback watcher (C5 logging)." + echo + echo "$c_head""Commands:$c_reset" + echo " $c_flag""install$c_reset Copy the watcher and wire it into kitty.conf" + echo " $c_flag""uninstall$c_reset Remove the managed block and the watcher file" + echo " $c_flag""status$c_reset Show wiring, watcher version, and C5 state" + echo " $c_flag""dismiss$c_reset Stop the per-session reminder" + echo " $c_flag""-h$c_reset, $c_flag""--help$c_reset Show this help message" + return 0 + end + + if not type -q kitty + echo "$c_err""kitty-logging: kitty is not installed$c_reset" >&2 + return 1 + end + + set -l kitty_dir (__kitty_logging_dir) + set -l conf "$kitty_dir/kitty.conf" + set -l src "$__fish_config_dir/kitty/fish-config-watcher.py" + set -l dst "$kitty_dir/fish-config-watcher.py" + set -l blk_begin "# >>> fish-config logging (managed — do not edit) >>>" + set -l blk_end "# <<< fish-config logging (managed) <<<" + + switch $cmd + case dismiss + set -U __fish_config_kitty_watcher_dismissed 1 + echo "$c_ok""→ Reminder dismissed.$c_reset Run $c_cmd""kitty-logging install$c_reset anytime to enable." + return 0 + + case status + echo "$c_head""Kitty logging status$c_reset" + echo " Config dir: $kitty_dir" + if test -f $conf; and command grep -qF -- "$blk_begin" $conf + echo " Managed block: $c_ok""present$c_reset" + else if __kitty_logging_has_watcher + echo " Managed block: $c_warn""absent$c_reset (a non-managed watcher line is present)" + else + echo " Managed block: $c_warn""absent$c_reset" + end + if test -f $dst + echo " Watcher file: $c_ok""installed$c_reset (version "(__kitty_logging_version $dst)")" + else + echo " Watcher file: $c_warn""not installed$c_reset" + end + if __fish_config_op_enabled __fish_config_op_logging + echo " C5 logging: $c_ok""enabled$c_reset" + else + echo " C5 logging: $c_warn""disabled$c_reset (capture is currently inert)" + end + return 0 + + case install + if not test -f $src + echo "$c_err""kitty-logging: watcher source not found at $src$c_reset" >&2 + return 1 + end + if not command mkdir -p $kitty_dir + echo "$c_err""kitty-logging: cannot create $kitty_dir$c_reset" >&2 + return 1 + end + + # Copy/refresh the watcher when missing or stale. + set -l copied 0 + if not test -f $dst + command cp $src $dst; and set copied 1 + else if test (__kitty_logging_version $src) -gt (__kitty_logging_version $dst) + command cp $src $dst; and set copied 1 + end + + # Already wired: idempotent no-op (watcher may still have refreshed). + if test -f $conf; and command grep -qF -- "$blk_begin" $conf + if test $copied -eq 1 + echo "$c_ok""→ Watcher updated.$c_reset Already wired in $conf." + else + echo "$c_ok""→ Already installed.$c_reset" + end + return 0 + end + + # Comment any active (non-managed) watcher line to avoid double-capture. + if __kitty_logging_has_watcher + command sed -i 's/^\([[:space:]]*\)\(watcher[[:space:]].*\)$/\1# disabled by fish-config: \2/' $conf + echo "$c_warn""→ Commented an existing watcher line in kitty.conf to avoid double-capture.$c_reset" + end + + # Append the managed block. + if not printf '%s\n' "" "$blk_begin" "watcher fish-config-watcher.py" "$blk_end" >>$conf + echo "$c_err""kitty-logging: failed to write $conf$c_reset" >&2 + return 1 + end + echo "$c_ok""→ Installed.$c_reset Watcher wired into $conf" + echo " $c_dim""Restart Kitty (new windows) for it to take effect.$c_reset" + if not __fish_config_op_enabled __fish_config_op_logging + echo " $c_warn""Note: C5 logging is disabled, so capture is currently inert.$c_reset" + end + return 0 + + case uninstall + if test -f $conf; and command grep -qF -- "$blk_begin" $conf + command sed -i '/^# >>> fish-config logging/,/^# <<< fish-config logging/d' $conf + echo "$c_ok""→ Removed managed block from $conf$c_reset" + else + echo "$c_dim""→ No managed block found in $conf$c_reset" + end + if test -f $dst + command rm -f $dst + echo "$c_ok""→ Removed $dst$c_reset" + end + return 0 + + case '*' + echo "$c_err""kitty-logging: unknown command '$cmd'$c_reset" >&2 + echo "Run $c_cmd""kitty-logging --help$c_reset for usage." >&2 + return 1 + end +end -- 2.52.0 From 78df24cba1d7627cef7a7643652d78e9d75ad655 Mon Sep 17 00:00:00 2001 From: rootiest Date: Tue, 16 Jun 2026 09:58:19 -0400 Subject: [PATCH 4/6] fix(kitty): harden kitty-logging install/uninstall/dismiss - abort install if the watcher copy fails (was wiring a block pointing at a missing file and reporting success) - handle dismiss before the kitty guard so it works without kitty installed - require both managed-block markers before the uninstall range-delete to prevent deleting to EOF on a malformed block --- functions/kitty-logging.fish | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/functions/kitty-logging.fish b/functions/kitty-logging.fish index 7747afc..c0047fc 100644 --- a/functions/kitty-logging.fish +++ b/functions/kitty-logging.fish @@ -53,6 +53,15 @@ function kitty-logging --description 'Install/manage the fish-config Kitty scrol return 0 end + # dismiss only sets a universal variable — it must work even where kitty is + # absent (e.g. shared dotfiles on a non-kitty machine), so handle it before + # the kitty guard. + if test "$cmd" = dismiss + set -U __fish_config_kitty_watcher_dismissed 1 + echo "$c_ok""→ Reminder dismissed.$c_reset Run $c_cmd""kitty-logging install$c_reset anytime to enable." + return 0 + end + if not type -q kitty echo "$c_err""kitty-logging: kitty is not installed$c_reset" >&2 return 1 @@ -66,11 +75,6 @@ function kitty-logging --description 'Install/manage the fish-config Kitty scrol set -l blk_end "# <<< fish-config logging (managed) <<<" switch $cmd - case dismiss - set -U __fish_config_kitty_watcher_dismissed 1 - echo "$c_ok""→ Reminder dismissed.$c_reset Run $c_cmd""kitty-logging install$c_reset anytime to enable." - return 0 - case status echo "$c_head""Kitty logging status$c_reset" echo " Config dir: $kitty_dir" @@ -103,12 +107,16 @@ function kitty-logging --description 'Install/manage the fish-config Kitty scrol return 1 end - # Copy/refresh the watcher when missing or stale. + # Copy/refresh the watcher when missing or stale. A failed copy must + # abort: otherwise we would wire a managed block pointing at a watcher + # file that does not exist and report success. set -l copied 0 - if not test -f $dst - command cp $src $dst; and set copied 1 - else if test (__kitty_logging_version $src) -gt (__kitty_logging_version $dst) - command cp $src $dst; and set copied 1 + if not test -f $dst; or test (__kitty_logging_version $src) -gt (__kitty_logging_version $dst) + if not command cp $src $dst + echo "$c_err""kitty-logging: failed to copy watcher to $dst$c_reset" >&2 + return 1 + end + set copied 1 end # Already wired: idempotent no-op (watcher may still have refreshed). @@ -140,9 +148,13 @@ function kitty-logging --description 'Install/manage the fish-config Kitty scrol return 0 case uninstall - if test -f $conf; and command grep -qF -- "$blk_begin" $conf + # Require both markers before the range-delete: a sed range with a + # missing end marker would delete from the begin marker to EOF. + if test -f $conf; and command grep -qF -- "$blk_begin" $conf; and command grep -qF -- "$blk_end" $conf command sed -i '/^# >>> fish-config logging/,/^# <<< fish-config logging/d' $conf echo "$c_ok""→ Removed managed block from $conf$c_reset" + else if test -f $conf; and command grep -qF -- "$blk_begin" $conf + echo "$c_warn""→ Managed block in $conf looks malformed (missing end marker); left it untouched.$c_reset" >&2 else echo "$c_dim""→ No managed block found in $conf$c_reset" end -- 2.52.0 From 914613c7513c8f500ae8508871fc53e9128745fa Mon Sep 17 00:00:00 2001 From: rootiest Date: Tue, 16 Jun 2026 10:00:19 -0400 Subject: [PATCH 5/6] feat(kitty): add per-session watcher setup reminder --- conf.d/kitty-watcher-reminder.fish | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 conf.d/kitty-watcher-reminder.fish diff --git a/conf.d/kitty-watcher-reminder.fish b/conf.d/kitty-watcher-reminder.fish new file mode 100644 index 0000000..ad8e37c --- /dev/null +++ b/conf.d/kitty-watcher-reminder.fish @@ -0,0 +1,21 @@ +# Copyright (C) 2026 Rootiest +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# C5 — Logging & Capture: a non-blocking, per-session reminder shown inside Kitty +# when the fish-config scrollback watcher is not yet set up. It never blocks the +# shell; it simply prints how to enable or silence it. The notice stops once the +# user runs `kitty-logging install` (a watcher line then exists in kitty.conf) or +# `kitty-logging dismiss` (sets the universal variable below). + +status is-interactive; or exit +type -q kitty; or exit +set -q KITTY_WINDOW_ID; or exit +__fish_config_op_enabled __fish_config_op_logging; or exit +__fish_variable_check __fish_config_kitty_watcher_dismissed; and exit +__kitty_logging_has_watcher; and exit + +set_color --bold yellow +echo "Kitty session logging is available but not set up." +set_color normal +echo " Enable: kitty-logging install" +echo " Silence: kitty-logging dismiss" -- 2.52.0 From 2ff553f7b6ac755919622c86a5119a53d4dd4463 Mon Sep 17 00:00:00 2001 From: rootiest Date: Tue, 16 Jun 2026 10:03:14 -0400 Subject: [PATCH 6/6] docs(kitty): document kitty-logging command and watcher reminder --- README.md | 16 ++++++++++++++++ docs/fish-config.md | 31 +++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/README.md b/README.md index 735604a..126d115 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,22 @@ Kitty watcher to skip capture via a sentinel file — no shell or terminal restart required. Logging is category **C5** in [Minimal Mode](#minimal-mode); `set -U __fish_config_opinionated 0` turns it off along with everything else. +The Kitty scrollback capture is provided by a watcher script that fish-config can +install and manage for you. Inside Kitty, if it isn't set up yet, you'll see a +one-time-per-session reminder. Manage it with: + +| Command | Action | +|---|---| +| `kitty-logging install` | Copy the watcher into your Kitty config and wire it into `kitty.conf` | +| `kitty-logging uninstall` | Remove the managed block and the watcher file | +| `kitty-logging status` | Show whether it's wired, the watcher version, and C5 state | +| `kitty-logging dismiss` | Stop the per-session reminder without installing | + +`install` adds a clearly-marked managed block to `kitty.conf` and comments out +any conflicting `watcher` line. It affects **new** Kitty windows (existing +windows keep their current watcher until restarted). Disabling C5 logging makes +the watcher inert without uninstalling it. + --- ## Documentation diff --git a/docs/fish-config.md b/docs/fish-config.md index dea563f..f9e77dc 100644 --- a/docs/fish-config.md +++ b/docs/fish-config.md @@ -64,6 +64,7 @@ The configuration is split across: first_run.fish One-time init: Fisher bootstrap, theme, welcome key_bindings.fish Custom key bindings and Vi mode logging-events.fish C5 --on-variable event handlers; syncs logging state at startup + kitty-watcher-reminder.fish C5 per-session reminder to set up the Kitty watcher paru-wrapper.fish Auto-generates ~/.local/bin/paru logging wrapper puffer.fish !! / !$ / ./ expansion (bundled from nickeb96/puffer-fish) tmux-logging.fish C5 starts tmux pipe-pane capture when fish runs inside tmux @@ -1385,6 +1386,29 @@ Add -i (interactive confirmation) to destructive commands: replay "source ~/.bashrc" replay "export FOO=bar" +### kitty-logging + + Synopsis: kitty-logging [install|uninstall|status|dismiss] [-h] + + Manages the Kitty scrollback watcher that powers C5 logging. Ships a + canonical, version-marked watcher and installs it into the Kitty config + directory, wiring it into kitty.conf through a sentinel-marked managed + block. Commenting out any conflicting watcher line avoids double-capture. + + Commands: + install Copy/refresh the watcher and add the managed block + uninstall Remove the managed block and the watcher file + status Show wiring, installed watcher version, and C5 state + dismiss Stop the per-session setup reminder + + Runtime capture stays governed by the C5 .logging_disabled sentinel, so + disabling __fish_config_op_logging makes the watcher inert without + uninstalling. Install affects new Kitty windows only. + + Example: + kitty-logging install + kitty-logging status + ### tmux-clean Synopsis: tmux-clean @@ -1733,6 +1757,13 @@ at exit, toggling __fish_config_op_logging takes effect on the next exit with no restart or sentinel coordination needed — the C5 guard is re-checked when the handler fires. +The Kitty watcher is managed by the kitty-logging command: it installs a +version-marked watcher (fish-config-watcher.py) into the Kitty config directory +and wires it into kitty.conf via a managed block. Inside Kitty, a non-blocking +per-session reminder points first-time users at `kitty-logging install` until +they install or run `kitty-logging dismiss`. Install affects new Kitty windows +only; runtime disable is still handled by the .logging_disabled sentinel. + Logging coordination via sentinel file C5 uses a sentinel file to synchronize state between the shell and -- 2.52.0