feat(c5): own the Kitty scrollback watcher (kitty-logging) #54
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,30 @@
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# SYNOPSIS
|
||||
# __kitty_logging_version <file>
|
||||
#
|
||||
# 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
|
||||
@@ -0,0 +1,172 @@
|
||||
# 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
|
||||
|
||||
# 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
|
||||
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 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. 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; 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).
|
||||
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
|
||||
# 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
|
||||
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
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user