From 0cbb7c6892bceb0ae85b01558e0711d5fc1bf539 Mon Sep 17 00:00:00 2001 From: rootiest Date: Thu, 11 Jun 2026 22:02:56 -0400 Subject: [PATCH 1/5] fix(logging): run paru/yay wrappers in PTY to restore progress bars Piping through tee caused libalpm to detect a non-TTY stdout and suppress download progress bars. Switch to script(1) with -q -e -c which allocates a pseudo-TTY so paru/yay see a real terminal, then strip ANSI escape sequences from the captured log for readability. Bump wrapper version to 2 to force regeneration on next shell start. --- conf.d/paru-wrapper.fish | 20 ++++++++++++++++---- conf.d/yay-wrapper.fish | 20 ++++++++++++++++---- docs/fish-config.md | 7 ++++--- 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/conf.d/paru-wrapper.fish b/conf.d/paru-wrapper.fish index 2bb7de5..178af4c 100644 --- a/conf.d/paru-wrapper.fish +++ b/conf.d/paru-wrapper.fish @@ -20,7 +20,7 @@ end set -l _paru_real /usr/bin/paru set -l _paru_wrapper "$HOME/.local/bin/paru" -set -l _paru_wrapper_version 1 +set -l _paru_wrapper_version 2 # Skip entirely if the real paru binary isn't present test -x $_paru_real; or return @@ -38,14 +38,24 @@ printf '%s\n' \ '#!/usr/bin/env bash' \ "# paru-wrapper-version: $_paru_wrapper_version" \ '# Auto-generated by conf.d/paru-wrapper.fish — do not edit by hand.' \ - '# Tees paru output to a timestamped log file and prunes old ones.' \ - 'set -o pipefail' \ + '# Runs paru in a PTY via script(1) so progress bars are preserved,' \ + '# then strips escape sequences from the captured log file.' \ '' \ 'log_dir="${SCROLLBACK_HISTORY_DIR:-$HOME/.terminal_history}"' \ 'mkdir -p "$log_dir"' \ 'log_file="$log_dir/paru_$(date +%Y-%m-%d_%H-%M-%S).log"' \ '' \ - '/usr/bin/paru "$@" 2>&1 | tee "$log_file"' \ + '# Build a safely-quoted command string for script(1).' \ + '# script(1) allocates a PTY so paru detects a real terminal and shows progress.' \ + 'cmd_str="/usr/bin/paru"' \ + 'for arg in "$@"; do' \ + ' cmd_str+=" $(printf '"'"'%q'"'"' "$arg")"' \ + 'done' \ + 'script -q -e -c "$cmd_str" "$log_file"' \ + 'exit_code=$?' \ + '' \ + '# Strip ANSI escape sequences and carriage returns for readable plain-text logs.' \ + 'sed -i '"'"'s/\x1b\[[0-9;?]*[a-zA-Z]//g; s/\r//g'"'"' "$log_file" 2>/dev/null || true' \ '' \ 'max_files="${SCROLLBACK_HISTORY_MAX_FILES:-100}"' \ 'mapfile -t logs < <(ls -1t "$log_dir"/paru_*.log 2>/dev/null)' \ @@ -53,6 +63,8 @@ printf '%s\n' \ 'for (( i = ${#logs[@]} - 1; i >= ${#logs[@]} - excess && i >= 0; i-- )); do' \ ' rm -f "${logs[$i]}"' \ 'done' \ + '' \ + 'exit $exit_code' \ > $_paru_wrapper chmod +x $_paru_wrapper diff --git a/conf.d/yay-wrapper.fish b/conf.d/yay-wrapper.fish index 4e16e05..0d63469 100644 --- a/conf.d/yay-wrapper.fish +++ b/conf.d/yay-wrapper.fish @@ -20,7 +20,7 @@ end set -l _yay_real /usr/bin/yay set -l _yay_wrapper "$HOME/.local/bin/yay" -set -l _yay_wrapper_version 1 +set -l _yay_wrapper_version 2 # Skip entirely if the real yay binary isn't present test -x $_yay_real; or return @@ -38,14 +38,24 @@ printf '%s\n' \ '#!/usr/bin/env bash' \ "# yay-wrapper-version: $_yay_wrapper_version" \ '# Auto-generated by conf.d/yay-wrapper.fish — do not edit by hand.' \ - '# Tees yay output to a timestamped log file and prunes old ones.' \ - 'set -o pipefail' \ + '# Runs yay in a PTY via script(1) so progress bars are preserved,' \ + '# then strips escape sequences from the captured log file.' \ '' \ 'log_dir="${SCROLLBACK_HISTORY_DIR:-$HOME/.terminal_history}"' \ 'mkdir -p "$log_dir"' \ 'log_file="$log_dir/yay_$(date +%Y-%m-%d_%H-%M-%S).log"' \ '' \ - '/usr/bin/yay "$@" 2>&1 | tee "$log_file"' \ + '# Build a safely-quoted command string for script(1).' \ + '# script(1) allocates a PTY so yay detects a real terminal and shows progress.' \ + 'cmd_str="/usr/bin/yay"' \ + 'for arg in "$@"; do' \ + ' cmd_str+=" $(printf '"'"'%q'"'"' "$arg")"' \ + 'done' \ + 'script -q -e -c "$cmd_str" "$log_file"' \ + 'exit_code=$?' \ + '' \ + '# Strip ANSI escape sequences and carriage returns for readable plain-text logs.' \ + 'sed -i '"'"'s/\x1b\[[0-9;?]*[a-zA-Z]//g; s/\r//g'"'"' "$log_file" 2>/dev/null || true' \ '' \ 'max_files="${SCROLLBACK_HISTORY_MAX_FILES:-100}"' \ 'mapfile -t logs < <(ls -1t "$log_dir"/yay_*.log 2>/dev/null)' \ @@ -53,6 +63,8 @@ printf '%s\n' \ 'for (( i = ${#logs[@]} - 1; i >= ${#logs[@]} - excess && i >= 0; i-- )); do' \ ' rm -f "${logs[$i]}"' \ 'done' \ + '' \ + 'exit $exit_code' \ > $_yay_wrapper chmod +x $_yay_wrapper diff --git a/docs/fish-config.md b/docs/fish-config.md index a24b2c4..aaeafed 100644 --- a/docs/fish-config.md +++ b/docs/fish-config.md @@ -216,7 +216,8 @@ scrollback snapshot to SCROLLBACK_HISTORY_DIR. Files are named: scrollback_YYYY-MM-DD_HH-MM-SS.log -The paru and yay wrappers (auto-generated in ~/.local/bin/) tee all output to: +The paru and yay wrappers (auto-generated in ~/.local/bin/) run inside a PTY +via script(1) so progress bars are preserved, and capture all output to: paru_YYYY-MM-DD_HH-MM-SS.log yay_YYYY-MM-DD_HH-MM-SS.log @@ -1615,9 +1616,9 @@ __fish_config_op_logging skips all capture and removes the logging wrappers. ─────────────────────────────────────────────────────────────────────────── Scrollback capture Terminal session output saved to: ~/.terminal_history/scrollback_YYYY-MM-DD_HH-MM-SS.log - paru wrapper All paru/AUR output teed to: + paru wrapper All paru/AUR output captured to: ~/.terminal_history/paru_YYYY-MM-DD_HH-MM-SS.log - yay wrapper All yay/AUR output teed to: + yay wrapper All yay/AUR output captured to: ~/.terminal_history/yay_YYYY-MM-DD_HH-MM-SS.log Kitty watcher watcher.py captures scrollback when Kitty closes From fc63b4f1b09f3350a56b671d9f584b92d6ffb907 Mon Sep 17 00:00:00 2001 From: rootiest Date: Thu, 11 Jun 2026 22:10:19 -0400 Subject: [PATCH 2/5] fix(logging): use col -bp to collapse progress bar redraws in AUR logs script(1) captures every \r-redrawn frame of the pacman progress bar as raw bytes. The previous sed pass only stripped ANSI codes, leaving all intermediate frames concatenated in the log. col -bp semantically replays the carriage-return overwrites and retains only the final state of each line; the sed pass then strips residual ANSI codes and the "Script started/done" header that script(1) writes to the file even under -q. Bump wrapper version to 3. --- conf.d/paru-wrapper.fish | 10 +++++++--- conf.d/yay-wrapper.fish | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/conf.d/paru-wrapper.fish b/conf.d/paru-wrapper.fish index 178af4c..4e675d5 100644 --- a/conf.d/paru-wrapper.fish +++ b/conf.d/paru-wrapper.fish @@ -20,7 +20,7 @@ end set -l _paru_real /usr/bin/paru set -l _paru_wrapper "$HOME/.local/bin/paru" -set -l _paru_wrapper_version 2 +set -l _paru_wrapper_version 3 # Skip entirely if the real paru binary isn't present test -x $_paru_real; or return @@ -54,8 +54,12 @@ printf '%s\n' \ 'script -q -e -c "$cmd_str" "$log_file"' \ 'exit_code=$?' \ '' \ - '# Strip ANSI escape sequences and carriage returns for readable plain-text logs.' \ - 'sed -i '"'"'s/\x1b\[[0-9;?]*[a-zA-Z]//g; s/\r//g'"'"' "$log_file" 2>/dev/null || true' \ + '# col -bp collapses CR-redrawn progress bar frames; sed strips ANSI codes and script(1) headers.' \ + 'if col -bp < "$log_file" | sed '"'"'s/\x1b\[[0-9;?]*[a-zA-Z]//g; /^Script \(started\|done\)/d'"'"' > "${log_file}.tmp" 2>/dev/null; then' \ + ' mv "${log_file}.tmp" "$log_file"' \ + 'else' \ + ' rm -f "${log_file}.tmp"' \ + 'fi' \ '' \ 'max_files="${SCROLLBACK_HISTORY_MAX_FILES:-100}"' \ 'mapfile -t logs < <(ls -1t "$log_dir"/paru_*.log 2>/dev/null)' \ diff --git a/conf.d/yay-wrapper.fish b/conf.d/yay-wrapper.fish index 0d63469..f2b9986 100644 --- a/conf.d/yay-wrapper.fish +++ b/conf.d/yay-wrapper.fish @@ -20,7 +20,7 @@ end set -l _yay_real /usr/bin/yay set -l _yay_wrapper "$HOME/.local/bin/yay" -set -l _yay_wrapper_version 2 +set -l _yay_wrapper_version 3 # Skip entirely if the real yay binary isn't present test -x $_yay_real; or return @@ -54,8 +54,12 @@ printf '%s\n' \ 'script -q -e -c "$cmd_str" "$log_file"' \ 'exit_code=$?' \ '' \ - '# Strip ANSI escape sequences and carriage returns for readable plain-text logs.' \ - 'sed -i '"'"'s/\x1b\[[0-9;?]*[a-zA-Z]//g; s/\r//g'"'"' "$log_file" 2>/dev/null || true' \ + '# col -bp collapses CR-redrawn progress bar frames; sed strips ANSI codes and script(1) headers.' \ + 'if col -bp < "$log_file" | sed '"'"'s/\x1b\[[0-9;?]*[a-zA-Z]//g; /^Script \(started\|done\)/d'"'"' > "${log_file}.tmp" 2>/dev/null; then' \ + ' mv "${log_file}.tmp" "$log_file"' \ + 'else' \ + ' rm -f "${log_file}.tmp"' \ + 'fi' \ '' \ 'max_files="${SCROLLBACK_HISTORY_MAX_FILES:-100}"' \ 'mapfile -t logs < <(ls -1t "$log_dir"/yay_*.log 2>/dev/null)' \ From bd0eac841374a8581e1fde8296a978f95134b541 Mon Sep 17 00:00:00 2001 From: rootiest Date: Thu, 11 Jun 2026 22:23:38 -0400 Subject: [PATCH 3/5] fix(logging): fix CR-collapse and script header filtering in AUR log post-processing col -bp is designed for backspace-overstriking (man pages), not ANSI terminal animations; it mangled escape sequences and garbled overprinted progress bar text. Replace with a perl -ne one-liner that: 1. Skips Script started/done header lines (next if matches, with -ne so next truly suppresses print, unlike -pe which still auto-prints) 2. Strips trailing \r from PTY \r\n line endings 3. Collapses CR-redrawn progress bar frames to their final state 4. Preserves all ANSI color codes so ov renders them correctly Bump wrapper version to 4 to force regeneration. --- conf.d/paru-wrapper.fish | 10 +++------- conf.d/yay-wrapper.fish | 10 +++------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/conf.d/paru-wrapper.fish b/conf.d/paru-wrapper.fish index 4e675d5..c801e65 100644 --- a/conf.d/paru-wrapper.fish +++ b/conf.d/paru-wrapper.fish @@ -20,7 +20,7 @@ end set -l _paru_real /usr/bin/paru set -l _paru_wrapper "$HOME/.local/bin/paru" -set -l _paru_wrapper_version 3 +set -l _paru_wrapper_version 4 # Skip entirely if the real paru binary isn't present test -x $_paru_real; or return @@ -54,12 +54,8 @@ printf '%s\n' \ 'script -q -e -c "$cmd_str" "$log_file"' \ 'exit_code=$?' \ '' \ - '# col -bp collapses CR-redrawn progress bar frames; sed strips ANSI codes and script(1) headers.' \ - 'if col -bp < "$log_file" | sed '"'"'s/\x1b\[[0-9;?]*[a-zA-Z]//g; /^Script \(started\|done\)/d'"'"' > "${log_file}.tmp" 2>/dev/null; then' \ - ' mv "${log_file}.tmp" "$log_file"' \ - 'else' \ - ' rm -f "${log_file}.tmp"' \ - 'fi' \ + '# Collapse CR-redrawn frames; keep ANSI codes for ov rendering; strip script(1) headers.' \ + 'perl -ne '"'"'next if /^Script (started|done)/; s/\r$//; s/.*\r([^\r]+)$/$1/; print'"'"' < "$log_file" > "${log_file}.tmp" 2>/dev/null && mv "${log_file}.tmp" "$log_file" || rm -f "${log_file}.tmp"' \ '' \ 'max_files="${SCROLLBACK_HISTORY_MAX_FILES:-100}"' \ 'mapfile -t logs < <(ls -1t "$log_dir"/paru_*.log 2>/dev/null)' \ diff --git a/conf.d/yay-wrapper.fish b/conf.d/yay-wrapper.fish index f2b9986..324945b 100644 --- a/conf.d/yay-wrapper.fish +++ b/conf.d/yay-wrapper.fish @@ -20,7 +20,7 @@ end set -l _yay_real /usr/bin/yay set -l _yay_wrapper "$HOME/.local/bin/yay" -set -l _yay_wrapper_version 3 +set -l _yay_wrapper_version 4 # Skip entirely if the real yay binary isn't present test -x $_yay_real; or return @@ -54,12 +54,8 @@ printf '%s\n' \ 'script -q -e -c "$cmd_str" "$log_file"' \ 'exit_code=$?' \ '' \ - '# col -bp collapses CR-redrawn progress bar frames; sed strips ANSI codes and script(1) headers.' \ - 'if col -bp < "$log_file" | sed '"'"'s/\x1b\[[0-9;?]*[a-zA-Z]//g; /^Script \(started\|done\)/d'"'"' > "${log_file}.tmp" 2>/dev/null; then' \ - ' mv "${log_file}.tmp" "$log_file"' \ - 'else' \ - ' rm -f "${log_file}.tmp"' \ - 'fi' \ + '# Collapse CR-redrawn frames; keep ANSI codes for ov rendering; strip script(1) headers.' \ + 'perl -ne '"'"'next if /^Script (started|done)/; s/\r$//; s/.*\r([^\r]+)$/$1/; print'"'"' < "$log_file" > "${log_file}.tmp" 2>/dev/null && mv "${log_file}.tmp" "$log_file" || rm -f "${log_file}.tmp"' \ '' \ 'max_files="${SCROLLBACK_HISTORY_MAX_FILES:-100}"' \ 'mapfile -t logs < <(ls -1t "$log_dir"/yay_*.log 2>/dev/null)' \ From 477a4ab265c883a2d1eb0000aa8694ddb66b88c9 Mon Sep 17 00:00:00 2001 From: rootiest Date: Thu, 11 Jun 2026 22:48:02 -0400 Subject: [PATCH 4/5] fix(logging): render AUR progress animations to clean logs via terminal emulator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pacman/paru download progress is a multi-line terminal animation: it repaints lines in place using ANSI cursor-movement (ESC[A) and erase-line (ESC[K) sequences, not just carriage returns. The previous line-wise regex approaches could not reconstruct this — frames either concatenated into one giant line (CR removed) or collapsed to blanks (CR kept), discarding the final 100% frame. Add scripts/clean_progress_log.py, a small dependency-free terminal screen-buffer emulator that replays the cursor movements against an in-memory grid and dumps the final static frame, preserving SGR color so logs still render with color in ov/bat/less -R. It also drops the script(1) header/footer. Both wrappers now pipe the raw PTY capture through this cleaner (bumped to version 5), falling back to stripping only the script(1) header when python3 is unavailable. Document the scripts/ dir and mechanism in AGENTS.md and docs/fish-config.md. --- conf.d/paru-wrapper.fish | 22 ++-- conf.d/yay-wrapper.fish | 22 ++-- docs/fish-config.md | 12 +- scripts/clean_progress_log.py | 225 ++++++++++++++++++++++++++++++++++ 4 files changed, 265 insertions(+), 16 deletions(-) create mode 100755 scripts/clean_progress_log.py diff --git a/conf.d/paru-wrapper.fish b/conf.d/paru-wrapper.fish index c801e65..a3d2a01 100644 --- a/conf.d/paru-wrapper.fish +++ b/conf.d/paru-wrapper.fish @@ -2,8 +2,9 @@ # SPDX-License-Identifier: AGPL-3.0-or-later # # Generates ~/.local/bin/paru on first run (and on version bump) when -# /usr/bin/paru is installed. The wrapper tees paru output to a -# timestamped log file and prunes old logs, mirroring smart_exit behavior. +# /usr/bin/paru is installed. The wrapper runs paru in a PTY so progress +# bars are preserved, renders the captured animation to a clean static log +# (via scripts/clean_progress_log.py), and prunes old logs. # Auto-generating a wrapper in ~/.local/bin is opinionated (C2 auto-exec). # Wrapper generation is also gated by C5 (Logging & Capture). @@ -20,7 +21,7 @@ end set -l _paru_real /usr/bin/paru set -l _paru_wrapper "$HOME/.local/bin/paru" -set -l _paru_wrapper_version 4 +set -l _paru_wrapper_version 5 # Skip entirely if the real paru binary isn't present test -x $_paru_real; or return @@ -38,8 +39,8 @@ printf '%s\n' \ '#!/usr/bin/env bash' \ "# paru-wrapper-version: $_paru_wrapper_version" \ '# Auto-generated by conf.d/paru-wrapper.fish — do not edit by hand.' \ - '# Runs paru in a PTY via script(1) so progress bars are preserved,' \ - '# then strips escape sequences from the captured log file.' \ + '# Runs paru in a PTY via script(1) so progress bars are preserved on screen,' \ + '# then renders the captured terminal animation to a clean static log.' \ '' \ 'log_dir="${SCROLLBACK_HISTORY_DIR:-$HOME/.terminal_history}"' \ 'mkdir -p "$log_dir"' \ @@ -54,8 +55,15 @@ printf '%s\n' \ 'script -q -e -c "$cmd_str" "$log_file"' \ 'exit_code=$?' \ '' \ - '# Collapse CR-redrawn frames; keep ANSI codes for ov rendering; strip script(1) headers.' \ - 'perl -ne '"'"'next if /^Script (started|done)/; s/\r$//; s/.*\r([^\r]+)$/$1/; print'"'"' < "$log_file" > "${log_file}.tmp" 2>/dev/null && mv "${log_file}.tmp" "$log_file" || rm -f "${log_file}.tmp"' \ + '# Render the captured terminal animation (progress bars repaint in place via' \ + '# cursor moves) down to its final static frame, preserving ANSI color. Falls' \ + '# back to dropping only the script(1) header/footer when python3 is missing.' \ + 'cleaner="${XDG_CONFIG_HOME:-$HOME/.config}/fish/scripts/clean_progress_log.py"' \ + 'if command -v python3 >/dev/null 2>&1 && [[ -f "$cleaner" ]]; then' \ + ' python3 "$cleaner" < "$log_file" > "${log_file}.tmp" 2>/dev/null && mv "${log_file}.tmp" "$log_file" || rm -f "${log_file}.tmp"' \ + 'else' \ + ' sed -i "/^Script \(started\|done\) on /d" "$log_file" 2>/dev/null || true' \ + 'fi' \ '' \ 'max_files="${SCROLLBACK_HISTORY_MAX_FILES:-100}"' \ 'mapfile -t logs < <(ls -1t "$log_dir"/paru_*.log 2>/dev/null)' \ diff --git a/conf.d/yay-wrapper.fish b/conf.d/yay-wrapper.fish index 324945b..1a42a6b 100644 --- a/conf.d/yay-wrapper.fish +++ b/conf.d/yay-wrapper.fish @@ -2,8 +2,9 @@ # SPDX-License-Identifier: AGPL-3.0-or-later # # Generates ~/.local/bin/yay on first run (and on version bump) when -# /usr/bin/yay is installed. The wrapper tees yay output to a -# timestamped log file and prunes old logs, mirroring smart_exit behavior. +# /usr/bin/yay is installed. The wrapper runs yay in a PTY so progress +# bars are preserved, renders the captured animation to a clean static log +# (via scripts/clean_progress_log.py), and prunes old logs. # Auto-generating a wrapper in ~/.local/bin is opinionated (C2 auto-exec). # Wrapper generation is also gated by C5 (Logging & Capture). @@ -20,7 +21,7 @@ end set -l _yay_real /usr/bin/yay set -l _yay_wrapper "$HOME/.local/bin/yay" -set -l _yay_wrapper_version 4 +set -l _yay_wrapper_version 5 # Skip entirely if the real yay binary isn't present test -x $_yay_real; or return @@ -38,8 +39,8 @@ printf '%s\n' \ '#!/usr/bin/env bash' \ "# yay-wrapper-version: $_yay_wrapper_version" \ '# Auto-generated by conf.d/yay-wrapper.fish — do not edit by hand.' \ - '# Runs yay in a PTY via script(1) so progress bars are preserved,' \ - '# then strips escape sequences from the captured log file.' \ + '# Runs yay in a PTY via script(1) so progress bars are preserved on screen,' \ + '# then renders the captured terminal animation to a clean static log.' \ '' \ 'log_dir="${SCROLLBACK_HISTORY_DIR:-$HOME/.terminal_history}"' \ 'mkdir -p "$log_dir"' \ @@ -54,8 +55,15 @@ printf '%s\n' \ 'script -q -e -c "$cmd_str" "$log_file"' \ 'exit_code=$?' \ '' \ - '# Collapse CR-redrawn frames; keep ANSI codes for ov rendering; strip script(1) headers.' \ - 'perl -ne '"'"'next if /^Script (started|done)/; s/\r$//; s/.*\r([^\r]+)$/$1/; print'"'"' < "$log_file" > "${log_file}.tmp" 2>/dev/null && mv "${log_file}.tmp" "$log_file" || rm -f "${log_file}.tmp"' \ + '# Render the captured terminal animation (progress bars repaint in place via' \ + '# cursor moves) down to its final static frame, preserving ANSI color. Falls' \ + '# back to dropping only the script(1) header/footer when python3 is missing.' \ + 'cleaner="${XDG_CONFIG_HOME:-$HOME/.config}/fish/scripts/clean_progress_log.py"' \ + 'if command -v python3 >/dev/null 2>&1 && [[ -f "$cleaner" ]]; then' \ + ' python3 "$cleaner" < "$log_file" > "${log_file}.tmp" 2>/dev/null && mv "${log_file}.tmp" "$log_file" || rm -f "${log_file}.tmp"' \ + 'else' \ + ' sed -i "/^Script \(started\|done\) on /d" "$log_file" 2>/dev/null || true' \ + 'fi' \ '' \ 'max_files="${SCROLLBACK_HISTORY_MAX_FILES:-100}"' \ 'mapfile -t logs < <(ls -1t "$log_dir"/yay_*.log 2>/dev/null)' \ diff --git a/docs/fish-config.md b/docs/fish-config.md index aaeafed..1c19733 100644 --- a/docs/fish-config.md +++ b/docs/fish-config.md @@ -60,6 +60,9 @@ The configuration is split across: completions/ Tab completion scripts integrations/ fzf.fish FZF Catppuccin theme and key binding config + scripts/ + clean_progress_log.py Renders script(1) typescripts (paru/yay progress + animations) to clean static logs, preserving color docs/ Offline documentation and compiled man page fish-config.md Primary source manual (terminal-readable) fish-config.1 Compiled man page (auto-generated by CI) @@ -216,8 +219,13 @@ scrollback snapshot to SCROLLBACK_HISTORY_DIR. Files are named: scrollback_YYYY-MM-DD_HH-MM-SS.log -The paru and yay wrappers (auto-generated in ~/.local/bin/) run inside a PTY -via script(1) so progress bars are preserved, and capture all output to: +The paru and yay wrappers (auto-generated in ~/.local/bin/) run the command +inside a PTY via script(1) so download progress bars are preserved on screen, +then render the captured terminal animation down to a clean static log via +scripts/clean_progress_log.py (a small terminal-screen emulator that replays +cursor movements, collapses repainted progress frames to their final state, +and preserves ANSI color). If python3 is unavailable the wrapper falls back to +dropping only the script(1) header/footer. Output is saved to: paru_YYYY-MM-DD_HH-MM-SS.log yay_YYYY-MM-DD_HH-MM-SS.log diff --git a/scripts/clean_progress_log.py b/scripts/clean_progress_log.py new file mode 100755 index 0000000..e68fd0a --- /dev/null +++ b/scripts/clean_progress_log.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 +# Copyright (C) 2026 Rootiest +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# SYNOPSIS +# clean_progress_log.py < typescript.raw > clean.log +# +# DESCRIPTION +# Renders a script(1) typescript containing terminal animations (pacman/paru +# download progress bars, which repaint in place using carriage returns and +# ANSI cursor-movement/erase sequences) down to a clean, static log that shows +# only the final state of each line. SGR color sequences are preserved so the +# output still renders with color in pagers such as ov, bat, or less -R. +# +# It implements a minimal terminal screen-buffer emulator: it replays the +# cursor movements against an in-memory grid of cells, so the concatenated +# redraw frames collapse to the final frame exactly as a real terminal would +# display them. script(1) "Script started/done" header and footer lines are +# dropped. +# +# RETURNS +# 0 Always (best-effort; unparseable bytes are passed through as text). +# +# EXAMPLE +# script -q -e -c 'paru -Syu' raw.log +# clean_progress_log.py < raw.log > clean.log + +import re +import sys + +# One CSI sequence: ESC [ . Covers SGR (m), cursor moves +# (A/B/C/D/E/F/G/H/f), erases (J/K), and private modes (?25l/h) alike. +_CSI = re.compile(r"\x1b\[([0-9;?]*)([A-Za-z])") +# OSC sequence (e.g. window title): ESC ] ... BEL or ESC ] ... ESC \ +_OSC = re.compile(r"\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)") +# script(1) header/footer lines to drop from the final render. +_SCRIPT_LINE = re.compile(r"^Script (started|done) on ") + + +class Screen: + """A minimal, unbounded-height terminal emulator. + + Each cell is a [sgr, char] pair, where sgr is the full active SGR escape + string in effect when the character was written ('' means default). The + grid grows downward as needed; relative cursor moves index into it, so + in-place repaints overwrite earlier frames just like a real terminal. + """ + + def __init__(self): + self.rows = [[]] + self.row = 0 + self.col = 0 + self.sgr = "" + + def _ensure_row(self, r): + while len(self.rows) <= r: + self.rows.append([]) + + def _ensure_col(self, c): + row = self.rows[self.row] + while len(row) <= c: + row.append(["", " "]) + + def write_char(self, ch): + self._ensure_row(self.row) + self._ensure_col(self.col) + self.rows[self.row][self.col] = [self.sgr, ch] + self.col += 1 + + def newline(self): + # script(1) typescripts always emit CRLF (the PTY's ONLCR translates + # LF to CRLF on output), so a bare LF starts a fresh line. Reset the + # column to avoid "staircase" artifacts if a bare LF ever appears. + self.row += 1 + self.col = 0 + self._ensure_row(self.row) + + def carriage_return(self): + self.col = 0 + + def backspace(self): + if self.col > 0: + self.col -= 1 + + def tab(self): + self.col = (self.col // 8 + 1) * 8 + + def set_sgr(self, params): + # Treat a bare reset (ESC[0m / ESC[m) as clearing all attributes; + # otherwise adopt the new full sequence. pacman always emits complete + # attribute sets, so replacement (rather than merging) is exact here. + if params in ("", "0"): + self.sgr = "" + else: + self.sgr = "\x1b[" + params + "m" + + def csi(self, params, final): + if final == "m": + self.set_sgr(params) + return + # Numeric argument (default 1 for moves, 0 for erases). + nums = [int(p) for p in params.split(";") if p.isdigit()] + n = nums[0] if nums else None + if final == "A": + self.row = max(0, self.row - (n or 1)) + elif final == "B" or final == "E": + self.row += (n or 1) + self._ensure_row(self.row) + if final == "E": + self.col = 0 + elif final == "F": + self.row = max(0, self.row - (n or 1)) + self.col = 0 + elif final == "C": + self.col += (n or 1) + elif final == "D": + self.col = max(0, self.col - (n or 1)) + elif final == "G": + self.col = (n or 1) - 1 + elif final in ("H", "f"): + self.row = (nums[0] - 1) if len(nums) >= 1 else 0 + self.col = (nums[1] - 1) if len(nums) >= 2 else 0 + self._ensure_row(self.row) + elif final == "K": + self._erase_line(n or 0) + elif final == "J": + self._erase_display(n or 0) + # Any other final (private modes like ?25l/h, etc.) is ignored. + + def _erase_line(self, mode): + self._ensure_row(self.row) + row = self.rows[self.row] + if mode == 0: # cursor to end of line + del row[self.col:] + elif mode == 1: # start of line to cursor + for c in range(min(self.col + 1, len(row))): + row[c] = ["", " "] + elif mode == 2: # whole line + row.clear() + + def _erase_display(self, mode): + if mode == 2: # whole screen + self.rows = [[]] + self.row = 0 + self.col = 0 + elif mode == 0: # cursor to end of screen + self._erase_line(0) + del self.rows[self.row + 1:] + + def feed(self, data): + i = 0 + n = len(data) + while i < n: + ch = data[i] + if ch == "\x1b": + m = _CSI.match(data, i) + if m: + self.csi(m.group(1), m.group(2)) + i = m.end() + continue + m = _OSC.match(data, i) + if m: + i = m.end() + continue + # Charset designators (ESC ( B etc.) and other 2-char escapes. + if i + 1 < n and data[i + 1] in "()": + i += 3 + else: + i += 2 + continue + if ch == "\n": + self.newline() + elif ch == "\r": + self.carriage_return() + elif ch == "\b": + self.backspace() + elif ch == "\t": + self.tab() + elif ch == "\x07": # bell + pass + else: + self.write_char(ch) + i += 1 + + def render(self): + out_lines = [] + for row in self.rows: + last_sgr = "" + buf = [] + for sgr, ch in row: + if sgr != last_sgr: + if sgr == "": + buf.append("\x1b[0m") + else: + buf.append(sgr) + last_sgr = sgr + buf.append(ch) + if last_sgr != "": + buf.append("\x1b[0m") + line = "".join(buf) + # Strip trailing whitespace (and any trailing reset that follows it). + line = re.sub(r"[ \t]+(\x1b\[0m)?$", r"\1", line) + out_lines.append(line) + # Drop script(1) header/footer lines (compare against de-escaped text). + kept = [] + for line in out_lines: + plain = _CSI.sub("", line) + if _SCRIPT_LINE.match(plain): + continue + kept.append(line) + # Trim trailing blank lines. + while kept and kept[-1].strip() == "": + kept.pop() + return "\n".join(kept) + ("\n" if kept else "") + + +def main(): + data = sys.stdin.buffer.read().decode("utf-8", "replace") + screen = Screen() + screen.feed(data) + sys.stdout.write(screen.render()) + + +if __name__ == "__main__": + main() From 499bf672f87c8e1a4308c6528ecbbd4c7bf7b746 Mon Sep 17 00:00:00 2001 From: rootiest Date: Thu, 11 Jun 2026 22:57:30 -0400 Subject: [PATCH 5/5] chore(gitignore): add python patterns to gitignore --- .gitignore | 178 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) diff --git a/.gitignore b/.gitignore index b855c37..1bccd22 100644 --- a/.gitignore +++ b/.gitignore @@ -110,3 +110,181 @@ fish_variables config.local.fish # End of https://www.toptal.com/developers/gitignore/api/fish + +# id: gi-patterns-e390e9c720b3dda36906a2b4cb76ffdd +# Created by https://www.toptal.com/developers/gitignore/api/python +# Edit at https://www.toptal.com/developers/gitignore?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +# End of https://www.toptal.com/developers/gitignore/api/python