feat(c5): multiplexer session logging (tmux + zellij) #53

Merged
rootiest merged 6 commits from feat/tmux-logging into main 2026-06-16 05:30:03 +00:00
8 changed files with 277 additions and 6 deletions
+63 -1
View File
@@ -8,6 +8,7 @@ abbreviation system for keyboard-driven workflows.
## Table of Contents ## Table of Contents
- [Overview](#overview) - [Overview](#overview)
- [Session Logging](#session-logging)
- [Documentation](#documentation) - [Documentation](#documentation)
- [Installation](#installation) - [Installation](#installation)
- [Personalization](#personalization) - [Personalization](#personalization)
@@ -27,10 +28,71 @@ This config layers on top of the CachyOS base Fish configuration and adds:
- **Smart CLI wrappers** that prefer modern tools (`eza`, `bat`, `btop`, `dust`, `prettyping`) with graceful fallbacks - **Smart CLI wrappers** that prefer modern tools (`eza`, `bat`, `btop`, `dust`, `prettyping`) with graceful fallbacks
- **Auto Python venv** activation on directory change (direnv-aware) - **Auto Python venv** activation on directory change (direnv-aware)
- **Kitty terminal** deep integration for splits, tabs, and SSH - **Kitty terminal** deep integration for splits, tabs, and SSH
- **Automatic session logging** — terminal scrollback, multiplexer panes (tmux/zellij), and AUR-helper output are captured to `~/.terminal_history` (see the caution below and [Session Logging](#session-logging))
- **AI workflow** helpers for Claude and Antigravity session management - **AI workflow** helpers for Claude and Antigravity session management
- **WakaTime** shell activity tracking - **WakaTime** shell activity tracking
- **Opt-out toggles** for every opinionated component — see [Minimal Mode](#minimal-mode) - **Opt-out toggles** for every opinionated component — see [Minimal Mode](#minimal-mode)
> [!CAUTION]
> **This configuration logs your terminal sessions to disk by default.**
> Out of the box it silently captures terminal output to `~/.terminal_history`:
> Kitty scrollback when a window closes, live tmux pane streams, zellij pane
> snapshots on exit, and full `paru`/`yay` output. These logs can contain
> command output, file contents, and anything else printed to your terminal.
> Nothing is sent off your machine, but the files persist locally until pruned.
>
> To turn all logging off, set the C5 category variable:
>
> ```fish
> set -U __fish_config_op_logging off
> ```
>
> Or run **`config-toggle`** for an interactive menu to flip logging (and any
> other opinionated category) on or off — no variable names to remember.
>
> This takes effect immediately in every open shell. See [Session Logging](#session-logging)
> for exactly what is captured and where, and [Minimal Mode](#minimal-mode) for the
> full set of opt-out toggles.
---
## Session Logging
This config captures terminal output to `~/.terminal_history` (override with
`$SCROLLBACK_HISTORY_DIR`) so you can search back through past sessions. It is
**on by default**. Five sources feed it:
| Source | When it captures | Log file |
|---|---|---|
| Kitty scrollback | When a Kitty window/tab closes | `scrollback_<timestamp>.log` |
| tmux pane | Continuously while the pane is open (`pipe-pane`) | `tmux_<session>-w<win>-p<pane>_<timestamp>.log` |
| zellij pane | Snapshot taken on shell exit (`dump-screen`) | `zellij_<session>-p<pane>_<timestamp>.log` |
| `paru` wrapper | Every `paru` invocation | `paru_<timestamp>.log` |
| `yay` wrapper | Every `yay` invocation | `yay_<timestamp>.log` |
Old logs are pruned automatically to stay within `$SCROLLBACK_HISTORY_MAX_FILES`
(default 100) per source, and empty/trivial captures are discarded.
**These logs can contain secrets** — anything printed to your terminal (command
output, file dumps, tokens echoed to stdout) ends up in them. They never leave
your machine, but treat `~/.terminal_history` as sensitive.
Disable all of it with a single universal variable:
```fish
set -U __fish_config_op_logging off # disable; takes effect in every open shell
set -Ue __fish_config_op_logging # re-enable (erase the override)
```
Prefer an interactive interface? Run **`config-toggle`** for a full-screen
picker that flips logging — and every other opinionated category — on or off
per session or universally, without memorizing variable names.
Disabling also removes the generated `paru`/`yay` log wrappers and tells the
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.
--- ---
## Documentation ## Documentation
@@ -178,7 +240,7 @@ If you'd rather set them by hand, each category is controlled by a universal var
| `__fish_config_op_autoexec` | Startup side-effects: Fisher bootstrap, theme apply, `paru`/`yay` wrapper generation, auto venv activation, WakaTime hook | | `__fish_config_op_autoexec` | Startup side-effects: Fisher bootstrap, theme apply, `paru`/`yay` wrapper generation, auto venv activation, WakaTime hook |
| `__fish_config_op_overrides` | Vi mode, `exit``smart_exit`, `$PAGER`/`$MANPAGER`/`$CDPATH`, bang-bang history expansion, autopair, puffer, Starship prompt, theme colors | | `__fish_config_op_overrides` | Vi mode, `exit``smart_exit`, `$PAGER`/`$MANPAGER`/`$CDPATH`, bang-bang history expansion, autopair, puffer, Starship prompt, theme colors |
| `__fish_config_op_integrations` | Kitty/WezTerm window abbreviations, `done` notifications, `spwin`/`tab`/`split`, `hist`, `logs`, `upgrade`, WakaTime | | `__fish_config_op_integrations` | Kitty/WezTerm window abbreviations, `done` notifications, `spwin`/`tab`/`split`, `hist`, `logs`, `upgrade`, WakaTime |
| `__fish_config_op_logging` | Scrollback capture on exit, `paru`/`yay` AUR log wrappers, Kitty watcher capture (sentinel-file coordinated) | | `__fish_config_op_logging` | Scrollback capture on exit, tmux `pipe-pane` pane logging, zellij `dump-screen` capture on exit, `paru`/`yay` AUR log wrappers, Kitty watcher capture (sentinel-file coordinated) |
| `__fish_config_op_greeting` | Per-session `fish_greeting` (suppresses distro greetings such as CachyOS fastfetch by overriding with an empty function); first-run welcome banner | | `__fish_config_op_greeting` | Per-session `fish_greeting` (suppresses distro greetings such as CachyOS fastfetch by overriding with an empty function); first-run welcome banner |
| `__fish_config_opinionated` | Master switch — all six categories at once | | `__fish_config_opinionated` | Master switch — all six categories at once |
+14
View File
@@ -0,0 +1,14 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# C5 — Logging & Capture: starts a pipe-pane log for the current tmux pane
# when fish launches inside a tmux session. Each fish shell gets its own
# timestamped log file in SCROLLBACK_HISTORY_DIR (default: ~/.terminal_history).
# Naming: tmux_<session>-w<window>-p<pane>_YYYY-MM-DD_HH-MM-SS.log
__fish_config_op_enabled __fish_config_op_logging; or exit
status is-interactive; or exit
type -q tmux; or exit
set -q TMUX; or exit
_tmux_pipe_log
+24
View File
@@ -0,0 +1,24 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# C5 — Logging & Capture: registers a fish_exit handler that dumps the current
# Zellij pane's scrollback to a log file when the shell exits. Unlike tmux
# (streamed live via pipe-pane), Zellij has no continuous-capture facility, so
# we snapshot once on exit via `zellij action dump-screen --full`.
#
# The handler is registered whenever fish runs inside Zellij; the C5 logging
# guard is evaluated inside _zellij_dump_log at exit time, so toggling
# __fish_config_op_logging takes effect on the next exit without a restart
# (no sync_logging coordination needed, since there is no live stream to stop).
#
# Event-handler functions must be defined at startup (conf.d) so the --on-event
# binding is registered; autoloaded functions in functions/ never register.
status is-interactive; or exit
type -q zellij; or exit
set -q ZELLIJ; or exit
function __zellij_dump_on_exit --on-event fish_exit \
--description 'C5 event handler: dump Zellij pane scrollback on shell exit'
_zellij_dump_log
end
+46 -2
View File
@@ -32,12 +32,27 @@ A production-grade Fish shell configuration targeting Fish 4.x. It provides:
- Drop-in replacements for common Unix tools (ls, cat, rm, du, ping, less) - Drop-in replacements for common Unix tools (ls, cat, rm, du, ping, less)
- Deep Kitty and WezTerm terminal integration: tab/window/pane management from - Deep Kitty and WezTerm terminal integration: tab/window/pane management from
the command line the command line
- Scrollback history snapshots saved to ~/.terminal_history on session exit - Automatic session logging: terminal scrollback, tmux/zellij panes, and
paru/yay output captured to ~/.terminal_history (on by default; see below)
- Automatic Python virtualenv activation on directory change - Automatic Python virtualenv activation on directory change
- Cross-platform package management via pkg and fish-deps - Cross-platform package management via pkg and fish-deps
- AI session helpers for Claude Code and Antigravity - AI session helpers for Claude Code and Antigravity
- Catppuccin Mocha color theme throughout - Catppuccin Mocha color theme throughout
┌───────────────────────────────────────────────────────────────────────────┐
│ CAUTION - SESSION LOGGING IS ON BY DEFAULT │
│ │
│ This configuration silently records terminal output to │
│ ~/.terminal_history: Kitty scrollback on window close, live tmux pane │
│ streams, zellij pane snapshots on exit, and full paru/yay output. These │
│ logs can contain command output, file contents, and secrets printed to │
│ the terminal. Nothing leaves your machine, but the files persist locally. │
│ │
│ Disable all logging with: set -U __fish_config_op_logging off │
│ Prefer a menu? Run the interactive picker: config-toggle │
│ See Section 7 (C5 - Logging and Capture) for the full breakdown. │
└───────────────────────────────────────────────────────────────────────────┘
The configuration is split across: The configuration is split across:
config.fish Main entry point; sets env vars and PATH config.fish Main entry point; sets env vars and PATH
@@ -51,6 +66,8 @@ The configuration is split across:
logging-events.fish C5 --on-variable event handlers; syncs logging state at startup logging-events.fish C5 --on-variable event handlers; syncs logging state at startup
paru-wrapper.fish Auto-generates ~/.local/bin/paru logging wrapper paru-wrapper.fish Auto-generates ~/.local/bin/paru logging wrapper
puffer.fish !! / !$ / ./ expansion (bundled from nickeb96/puffer-fish) puffer.fish !! / !$ / ./ expansion (bundled from nickeb96/puffer-fish)
tmux-logging.fish C5 starts tmux pipe-pane capture when fish runs inside tmux
zellij-logging.fish C5 fish_exit handler dumping zellij pane scrollback on exit
sponge_privacy.fish Sponge privacy patterns; filters credentials from history sponge_privacy.fish Sponge privacy patterns; filters credentials from history
starship.fish fish_prompt with OSC 133 shell-integration markers starship.fish fish_prompt with OSC 133 shell-integration markers
tailscale.fish Tailscale CLI tab completions tailscale.fish Tailscale CLI tab completions
@@ -1552,6 +1569,10 @@ Examples:
set -U __fish_config_op_greeting 1 set -U __fish_config_op_greeting 1
# (erase both to go back to full-flavor defaults) # (erase both to go back to full-flavor defaults)
For an interactive alternative to setting these variables by hand, run
config-toggle — a full-screen TUI that flips any category (including C5
logging) on or off, per session or universally. See its entry in Section 5.
Notes: Notes:
- Command shadows (rm, cat, ls, ...) react immediately; conf.d-level - Command shadows (rm, cat, ls, ...) react immediately; conf.d-level
@@ -1678,19 +1699,40 @@ silently failing.
#### C5 — Logging and Capture #### C5 — Logging and Capture
Three components capture shell output to disk. Disabling Five components capture shell output to disk. Disabling
__fish_config_op_logging skips all capture and removes the logging wrappers. __fish_config_op_logging skips all capture and removes the logging wrappers.
Component What it captures Component What it captures
─────────────────────────────────────────────────────────────────────────── ───────────────────────────────────────────────────────────────────────────
Scrollback capture Terminal session output saved to: Scrollback capture Terminal session output saved to:
~/.terminal_history/scrollback_YYYY-MM-DD_HH-MM-SS.log ~/.terminal_history/scrollback_YYYY-MM-DD_HH-MM-SS.log
tmux pane capture Continuous pane stream via pipe-pane, saved to:
~/.terminal_history/tmux_<session>-w<win>-p<pane>_YYYY-MM-DD_HH-MM-SS.log
zellij pane capture Pane scrollback snapshot on shell exit, saved to:
~/.terminal_history/zellij_<session>-p<pane>_YYYY-MM-DD_HH-MM-SS.log
paru wrapper All paru/AUR output captured to: paru wrapper All paru/AUR output captured to:
~/.terminal_history/paru_YYYY-MM-DD_HH-MM-SS.log ~/.terminal_history/paru_YYYY-MM-DD_HH-MM-SS.log
yay wrapper All yay/AUR output captured to: yay wrapper All yay/AUR output captured to:
~/.terminal_history/yay_YYYY-MM-DD_HH-MM-SS.log ~/.terminal_history/yay_YYYY-MM-DD_HH-MM-SS.log
Kitty watcher watcher.py captures scrollback when Kitty closes Kitty watcher watcher.py captures scrollback when Kitty closes
The tmux capture starts automatically when fish launches inside any tmux
pane ($TMUX is set). It uses tmux's native pipe-pane to stream all pane
output directly to disk without an intermediate process. Each fish shell
session gets its own log file; a new log is created on each shell start
(including exec fish and new splits). Before each new log, the oldest
tmux_*.log files are pruned (by modification time) to keep the total within
SCROLLBACK_HISTORY_MAX_FILES, matching the paru/yay wrapper behaviour.
The zellij capture works differently: Zellij has no live output-streaming
facility like pipe-pane, so the log is taken as a one-shot snapshot when the
shell exits, via `zellij action dump-screen --full`. A fish_exit handler
(registered whenever $ZELLIJ is set) writes the pane's full scrollback and
then prunes old zellij_*.log files the same way. Because the capture happens
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.
Logging coordination via sentinel file Logging coordination via sentinel file
C5 uses a sentinel file to synchronize state between the shell and C5 uses a sentinel file to synchronize state between the shell and
@@ -1705,11 +1747,13 @@ Disabling __fish_config_op_logging:
3. Kitty's watcher.py reads the sentinel on each save attempt and 3. Kitty's watcher.py reads the sentinel on each save attempt and
skips capture — no Kitty restart required. skips capture — no Kitty restart required.
4. smart_exit stops saving scrollback logs. 4. smart_exit stops saving scrollback logs.
5. Stops tmux pipe-pane capture in every open fish shell inside tmux.
Re-enabling __fish_config_op_logging: Re-enabling __fish_config_op_logging:
1. Removes the sentinel in every open shell. 1. Removes the sentinel in every open shell.
2. Regenerates paru/yay logging wrappers in ~/.local/bin/. 2. Regenerates paru/yay logging wrappers in ~/.local/bin/.
3. Kitty watcher resumes capture on the next session exit. 3. Kitty watcher resumes capture on the next session exit.
4. Restarts tmux pipe-pane capture in every open fish shell inside tmux.
Changes propagate to all running shells through an event handler that fires Changes propagate to all running shells through an event handler that fires
whenever __fish_config_op_logging changes — no shell restart needed. whenever __fish_config_op_logging changes — no shell restart needed.
+12 -3
View File
@@ -6,8 +6,9 @@
# #
# DESCRIPTION # DESCRIPTION
# Synchronises C5 logging state: creates or removes the Kitty sentinel # Synchronises C5 logging state: creates or removes the Kitty sentinel
# file ($XDG_CONFIG_HOME/fish/.logging_disabled) and generates or removes # file ($XDG_CONFIG_HOME/fish/.logging_disabled), generates or removes the
# the paru/yay AUR-helper log wrappers based on the combined value of # paru/yay AUR-helper log wrappers, and starts or stops tmux pipe-pane
# capture for the current pane — all based on the combined value of
# __fish_config_opinionated (master) and __fish_config_op_logging (C5). # __fish_config_opinionated (master) and __fish_config_op_logging (C5).
# Called by --on-variable event handlers whenever either variable changes. # Called by --on-variable event handlers whenever either variable changes.
# Safe to call at any time; wrapper removal only affects files bearing # Safe to call at any time; wrapper removal only affects files bearing
@@ -18,7 +19,7 @@
# #
# EXAMPLE # EXAMPLE
# __fish_config_sync_logging # __fish_config_sync_logging
function __fish_config_sync_logging --description 'Sync C5 logging state: sentinel file and paru/yay wrappers' function __fish_config_sync_logging --description 'Sync C5 logging state: sentinel file, paru/yay wrappers, and tmux pipe-pane'
set -l config_home $XDG_CONFIG_HOME set -l config_home $XDG_CONFIG_HOME
if test -z "$config_home" if test -z "$config_home"
set config_home "$HOME/.config" set config_home "$HOME/.config"
@@ -32,6 +33,9 @@ function __fish_config_sync_logging --description 'Sync C5 logging state: sentin
# Logging enabled: remove sentinel and regenerate wrappers if binaries exist # Logging enabled: remove sentinel and regenerate wrappers if binaries exist
rm -f $sentinel rm -f $sentinel
# Restart tmux pipe-pane for the current pane if inside tmux
_tmux_pipe_log
if test -x /usr/bin/paru if test -x /usr/bin/paru
mkdir -p (dirname $paru_wrapper) mkdir -p (dirname $paru_wrapper)
printf '%s\n' \ printf '%s\n' \
@@ -95,5 +99,10 @@ function __fish_config_sync_logging --description 'Sync C5 logging state: sentin
and grep -q "# yay-wrapper-version:" $yay_wrapper 2>/dev/null and grep -q "# yay-wrapper-version:" $yay_wrapper 2>/dev/null
rm -f $yay_wrapper rm -f $yay_wrapper
end end
# Stop tmux pipe-pane for the current pane if inside tmux
if set -q TMUX
tmux pipe-pane 2>/dev/null
end
end end
end end
+45
View File
@@ -0,0 +1,45 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# SYNOPSIS
# _prune_terminal_logs <prefix>
#
# DESCRIPTION
# Removes the oldest "<prefix>_*.log" files in SCROLLBACK_HISTORY_DIR
# (default ~/.terminal_history) so the total count for that prefix stays
# within SCROLLBACK_HISTORY_MAX_FILES (default 100). Files are sorted by
# modification time, so the most recently written logs are kept — actively
# appended logs (e.g. a tmux pipe-pane stream) survive.
#
# Uses `command ls`/`command rm` to bypass the C1 shadows: the bare `ls` is
# the eza wrapper, which injects OSC-8 hyperlink escapes into paths, and the
# bare `rm` is the trash wrapper. The glob is expanded via `set` first so a
# no-match (empty dir / first run) yields an empty list instead of a hard
# "No matches for wildcard" error.
#
# ARGUMENTS
# prefix Log-name prefix to prune (e.g. tmux, zellij)
#
# RETURNS
# 0 Always
#
# EXAMPLE
# _prune_terminal_logs tmux
function _prune_terminal_logs --argument-names prefix --description 'Prune oldest <prefix>_*.log files beyond the configured max'
set -q prefix[1]; or return 0
set -l log_dir (set -q SCROLLBACK_HISTORY_DIR; and echo $SCROLLBACK_HISTORY_DIR; or echo "$HOME/.terminal_history")
set -l max_files (set -q SCROLLBACK_HISTORY_MAX_FILES; and echo $SCROLLBACK_HISTORY_MAX_FILES; or echo 100)
test -d $log_dir; or return 0
# Glob via `set` so a no-match yields an empty list rather than an error.
set -l logs $log_dir/$prefix"_"*.log
set -q logs[1]; or return 0
# Re-sort by mtime, oldest first. Pass explicit files (no glob) so ls can
# never fail on no-match; `command ls` avoids the eza OSC-8 corruption.
set logs (command ls -1tr $logs 2>/dev/null)
if test (count $logs) -gt $max_files
set -l excess (math (count $logs) - $max_files)
command rm -f $logs[1..$excess]
end
end
+33
View File
@@ -0,0 +1,33 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# SYNOPSIS
# _tmux_pipe_log
#
# DESCRIPTION
# Starts tmux pipe-pane capture for the current pane, streaming its output
# to a timestamped log in SCROLLBACK_HISTORY_DIR (default ~/.terminal_history).
# Old tmux_*.log files are pruned via _prune_terminal_logs to stay within
# SCROLLBACK_HISTORY_MAX_FILES. No-op when not inside a tmux session ($TMUX
# unset) or tmux is missing. Shared by conf.d/tmux-logging.fish (shell
# startup) and __fish_config_sync_logging (C5 re-enable) so both stay in sync.
#
# RETURNS
# 0 Always
#
# EXAMPLE
# _tmux_pipe_log
function _tmux_pipe_log --description 'Start tmux pipe-pane capture for the current pane, with pruning'
set -q TMUX; or return 0
type -q tmux; or return 0
set -l log_dir (set -q SCROLLBACK_HISTORY_DIR; and echo $SCROLLBACK_HISTORY_DIR; or echo "$HOME/.terminal_history")
set -l pane_id (tmux display-message -p '#{session_name}-w#{window_index}-p#{pane_index}' 2>/dev/null)
set -l timestamp (date "+%Y-%m-%d_%H-%M-%S")
set -l log_file "$log_dir/tmux_"$pane_id"_"$timestamp".log"
mkdir -p $log_dir
_prune_terminal_logs tmux
tmux pipe-pane "cat >> $log_file" 2>/dev/null
end
+40
View File
@@ -0,0 +1,40 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# SYNOPSIS
# _zellij_dump_log
#
# DESCRIPTION
# Captures the current Zellij pane's scrollback to a timestamped log in
# SCROLLBACK_HISTORY_DIR (default ~/.terminal_history). Zellij has no live
# output-streaming facility like tmux's pipe-pane, so this performs a one-shot
# `zellij action dump-screen --full` — intended to run on shell exit. Old
# zellij_*.log files are pruned via _prune_terminal_logs to stay within
# SCROLLBACK_HISTORY_MAX_FILES.
#
# The C5 logging guard (__fish_config_op_logging) is evaluated here, at call
# time, so toggling logging takes effect on the next exit without a restart.
# No-op when logging is disabled, not inside Zellij ($ZELLIJ unset), or the
# zellij binary is missing.
#
# RETURNS
# 0 Always
#
# EXAMPLE
# _zellij_dump_log
function _zellij_dump_log --description 'Dump the current Zellij pane scrollback to a log file, with pruning'
__fish_config_op_enabled __fish_config_op_logging; or return 0
set -q ZELLIJ; or return 0
type -q zellij; or return 0
set -l log_dir (set -q SCROLLBACK_HISTORY_DIR; and echo $SCROLLBACK_HISTORY_DIR; or echo "$HOME/.terminal_history")
set -l session (set -q ZELLIJ_SESSION_NAME; and echo $ZELLIJ_SESSION_NAME; or echo unknown)
set -l pane_id (set -q ZELLIJ_PANE_ID; and echo $ZELLIJ_PANE_ID; or echo unknown)
set -l timestamp (date "+%Y-%m-%d_%H-%M-%S")
set -l log_file "$log_dir/zellij_"$session"-p"$pane_id"_"$timestamp".log"
mkdir -p $log_dir
zellij action dump-screen --full "$log_file" 2>/dev/null
_prune_terminal_logs zellij
end