mirror of
https://github.com/nmasur/dotfiles
synced 2026-08-30 12:37:34 +00:00
add lag-triage diagnostic toolkit for post-TUI typing lag
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
cfg = config.nmasur.presets.programs.lag-triage;
|
||||
|
||||
term-probe = pkgs.writeScriptBin "term-probe" ''
|
||||
#!${lib.getExe pkgs.python3}
|
||||
${builtins.readFile ./term_probe.py}
|
||||
'';
|
||||
in
|
||||
|
||||
{
|
||||
|
||||
options.nmasur.presets.programs.lag-triage.enable =
|
||||
lib.mkEnableOption "Terminal input-lag triage tools";
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
|
||||
home.packages = [ term-probe ];
|
||||
|
||||
programs.fish.functions = {
|
||||
lag-triage = {
|
||||
description = "Diagnose post-TUI typing lag in the current shell";
|
||||
body = builtins.readFile ./lag-triage.fish;
|
||||
};
|
||||
unlag = {
|
||||
description = "Reset terminal state left behind by a TUI";
|
||||
body = builtins.readFile ./unlag.fish;
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
# Guided diagnosis for the post-TUI typing-lag problem (Ghostty + Zellij + fish).
|
||||
# Run this IN THE LAGGING SHELL the moment you notice the lag, BEFORE starting
|
||||
# a new shell. It captures evidence, then applies targeted resets one at a time
|
||||
# so the stage that cures the lag identifies the layer holding stuck state.
|
||||
# Everything is logged for filing an upstream issue.
|
||||
|
||||
set -l logdir ~/.local/state/lag-triage
|
||||
mkdir -p $logdir
|
||||
set -l logfile $logdir/(date +%Y%m%d-%H%M%S).log
|
||||
|
||||
function _lt --inherit-variable logfile
|
||||
echo $argv | tee -a $logfile
|
||||
end
|
||||
|
||||
function _lt_ask --inherit-variable logfile
|
||||
# usage: _lt_ask VARNAME prompt... -> sets global $VARNAME (default: skip)
|
||||
set -l __name $argv[1]
|
||||
read -g -P "$argv[2..] " $__name
|
||||
or set -g $__name skip
|
||||
test -z "$$__name"; and set -g $__name skip
|
||||
echo "ANSWER $__name: $$__name" >>$logfile
|
||||
end
|
||||
|
||||
_lt "== lag-triage "(date)" =="
|
||||
_lt "Log: $logfile"
|
||||
_lt "Answer y / n, or press Enter to skip a question."
|
||||
_lt ""
|
||||
|
||||
# ---- 1. Context -------------------------------------------------------------
|
||||
_lt_ask ans_tui "Which TUI did you just exit (nvim/jjui/yazi/other)?"
|
||||
_lt_ask ans_launch "Launched via (f)loating-pane keybind or (c)ommand typed in this shell?"
|
||||
|
||||
# ---- 2. Snapshot ------------------------------------------------------------
|
||||
begin
|
||||
echo "-- snapshot --"
|
||||
fish --version
|
||||
echo "fish pid: $fish_pid, started: "(ps -o lstart= -p $fish_pid 2>/dev/null)
|
||||
zellij --version 2>/dev/null
|
||||
echo "escape delay: '$fish_escape_delay_ms' sequence delay: '$fish_sequence_key_delay_ms'"
|
||||
env | grep -iE '^(TERM|ZELLIJ|GHOSTTY|COLORTERM)' | sort
|
||||
echo "-- status features --"
|
||||
status features
|
||||
echo "-- stty -a --"
|
||||
stty -a
|
||||
end >>$logfile 2>&1
|
||||
_lt "Captured shell + environment snapshot."
|
||||
|
||||
# ---- 3. Terminal state below the shell --------------------------------------
|
||||
_lt ""
|
||||
_lt "Querying terminal state (takes a few seconds)..."
|
||||
term-probe report 2>&1 | tee -a $logfile
|
||||
_lt ""
|
||||
_lt " ^ Things to look for: kitty flags with a reply > 0, modifyOtherKeys > 1,"
|
||||
_lt " any mouse/alternate-screen mode SET while at a shell prompt, or a slow"
|
||||
_lt " DA1 round-trip (> 100 ms means the input path itself is delayed)."
|
||||
|
||||
# ---- 4. Raw keystroke capture (bypasses fish entirely) ----------------------
|
||||
_lt ""
|
||||
_lt "Raw input capture: type ~10 characters at a steady pace, including one"
|
||||
_lt "ESC press and one arrow key. This shows the exact bytes this pane delivers"
|
||||
_lt "and their timing, with fish's input handling out of the picture."
|
||||
term-probe keylog 2>&1 | tee -a $logfile
|
||||
_lt_ask ans_keylog_instant "Did each keypress appear INSTANTLY in the capture? (y/n)"
|
||||
_lt_ask ans_keylog_plain "Were plain letters single plain bytes like b'a' (not escape sequences)? (y/n)"
|
||||
|
||||
# ---- 5. Scope ---------------------------------------------------------------
|
||||
_lt ""
|
||||
_lt_ask ans_scope_pane "Optional: open a NEW zellij pane/tab and type — laggy there too? (y/n)"
|
||||
_lt_ask ans_scope_window "Optional: type in a separate Ghostty window (outside this zellij session) — laggy? (y/n)"
|
||||
|
||||
# ---- 6. Staged resets -------------------------------------------------------
|
||||
# Each stage resets one category of state a TUI could have left behind.
|
||||
# The first stage that cures the lag names the culprit.
|
||||
set -l fixed none
|
||||
|
||||
_lt ""
|
||||
_lt "Now applying resets one at a time. After each, type into the test prompt"
|
||||
_lt "to judge whether the lag is gone."
|
||||
|
||||
if test $fixed = none
|
||||
_lt ""
|
||||
_lt "Stage A - kitty keyboard protocol: pop stack + clear all flags"
|
||||
printf '\e[<9u\e[=0;1u'
|
||||
_lt_ask ans_stage_a " Test typing here, then Enter — lag gone? (y/n)"
|
||||
test "$ans_stage_a" = y; and set fixed "A (kitty keyboard state)"
|
||||
end
|
||||
|
||||
if test $fixed = none
|
||||
_lt "Stage B - modifyOtherKeys off"
|
||||
printf '\e[>4;0m'
|
||||
_lt_ask ans_stage_b " Test typing here, then Enter — lag gone? (y/n)"
|
||||
test "$ans_stage_b" = y; and set fixed "B (modifyOtherKeys)"
|
||||
end
|
||||
|
||||
if test $fixed = none
|
||||
_lt "Stage C - normal keypad + normal cursor keys"
|
||||
printf '\e>\e[?1l'
|
||||
_lt_ask ans_stage_c " Test typing here, then Enter — lag gone? (y/n)"
|
||||
test "$ans_stage_c" = y; and set fixed "C (application keypad/cursor mode)"
|
||||
end
|
||||
|
||||
if test $fixed = none
|
||||
_lt "Stage D - disable mouse, focus reporting, synchronized output"
|
||||
printf '\e[?1000l\e[?1001l\e[?1002l\e[?1003l\e[?1005l\e[?1006l\e[?1015l\e[?1016l\e[?1004l\e[?2026l'
|
||||
_lt_ask ans_stage_d " Test typing here, then Enter — lag gone? (y/n)"
|
||||
test "$ans_stage_d" = y; and set fixed "D (mouse/focus/sync modes)"
|
||||
end
|
||||
|
||||
if test $fixed = none
|
||||
_lt "Stage E - leave alternate screen"
|
||||
printf '\e[?1049l'
|
||||
_lt_ask ans_stage_e " Test typing here, then Enter — lag gone? (y/n)"
|
||||
test "$ans_stage_e" = y; and set fixed "E (alternate screen)"
|
||||
end
|
||||
|
||||
if test $fixed = none
|
||||
_lt "Stage F - stty sane (line-discipline reset)"
|
||||
stty sane
|
||||
_lt_ask ans_stage_f " Test typing here, then Enter — lag gone? (y/n)"
|
||||
test "$ans_stage_f" = y; and set fixed "F (termios/line discipline)"
|
||||
end
|
||||
|
||||
if test $fixed = none
|
||||
_lt "Stage G - DECSTR soft terminal reset"
|
||||
printf '\e[!p'
|
||||
_lt_ask ans_stage_g " Test typing here, then Enter — lag gone? (y/n)"
|
||||
test "$ans_stage_g" = y; and set fixed "G (DECSTR-resettable mode)"
|
||||
end
|
||||
|
||||
# ---- 7. Verdict --------------------------------------------------------------
|
||||
_lt ""
|
||||
_lt "== Verdict =="
|
||||
if test $fixed != none
|
||||
_lt "Lag cleared by stage $fixed."
|
||||
_lt "That state was stuck BELOW fish — in the Zellij pane or relayed to"
|
||||
_lt "Ghostty — and the TUI you exited ($ans_tui) failed to restore it, or"
|
||||
_lt "Zellij failed to restore it when the pane closed."
|
||||
_lt "Re-probing terminal state after the fix for comparison:"
|
||||
term-probe report 2>&1 | tee -a $logfile
|
||||
_lt ""
|
||||
_lt "-> File this log against zellij (or ghostty, if a separate window also"
|
||||
_lt " lagged). The before/after probe diff pinpoints the exact stuck mode."
|
||||
else if test "$ans_keylog_instant" = y; and test "$ans_keylog_plain" = y
|
||||
_lt "Raw input reaches this pane instantly as plain bytes, and no terminal"
|
||||
_lt "state reset helps: the lag lives INSIDE this fish process (reader state)."
|
||||
_lt "Confirm now: run 'exec fish' — if that cures it, it is fish-internal."
|
||||
_lt ""
|
||||
_lt "-> To catch it in the act, run your next long-lived shell as:"
|
||||
_lt " FISH_DEBUG='reader,term-support' FISH_DEBUG_OUTPUT=$logdir/fish-debug.log fish"
|
||||
_lt " then re-run lag-triage when it recurs and file both logs to fish-shell."
|
||||
else
|
||||
_lt "Keystrokes were delayed or arrived as escape sequences BEFORE fish saw"
|
||||
_lt "them: the problem is in Zellij (client stdin parser / server) or Ghostty."
|
||||
_lt " new pane also laggy: $ans_scope_pane (y -> session-wide, not this pane)"
|
||||
_lt " separate window laggy: $ans_scope_window (y -> Ghostty itself)"
|
||||
_lt "-> File this log against zellij; include the keylog byte capture."
|
||||
end
|
||||
_lt ""
|
||||
_lt "Full log: $logfile"
|
||||
|
||||
functions -e _lt _lt_ask
|
||||
@@ -0,0 +1,165 @@
|
||||
"""Probe the terminal state of the current pane, below the shell.
|
||||
|
||||
Modes:
|
||||
report - query kitty-keyboard flags, modifyOtherKeys, and DEC private
|
||||
modes directly on /dev/tty, reporting each reply (or lack of
|
||||
one) and its round-trip latency. Answers may come from Zellij
|
||||
(pane state) or be relayed from Ghostty (window state).
|
||||
keylog - raw-mode keystroke capture: prints the exact bytes and
|
||||
inter-key latency for every keypress, bypassing the shell's
|
||||
input machinery entirely. Press q to finish.
|
||||
|
||||
Used by the `lag-triage` fish function to pin down which layer
|
||||
(fish / zellij / ghostty) is holding stuck state when typing lags
|
||||
after a TUI exits.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import select
|
||||
import sys
|
||||
import termios
|
||||
import time
|
||||
import tty
|
||||
|
||||
# DECRQM reply values
|
||||
DECRQM_VALUES = {
|
||||
"0": "not recognized",
|
||||
"1": "SET",
|
||||
"2": "reset",
|
||||
"3": "permanently set",
|
||||
"4": "permanently reset",
|
||||
}
|
||||
|
||||
DEC_MODES = [
|
||||
(1, "application cursor keys (DECCKM)"),
|
||||
(25, "cursor visible"),
|
||||
(1000, "mouse click reporting"),
|
||||
(1002, "mouse drag reporting"),
|
||||
(1003, "mouse all-motion reporting"),
|
||||
(1004, "focus reporting"),
|
||||
(1006, "SGR mouse encoding"),
|
||||
(1049, "alternate screen"),
|
||||
(2004, "bracketed paste"),
|
||||
(2026, "synchronized output"),
|
||||
(2031, "color theme reporting"),
|
||||
]
|
||||
|
||||
QUERIES = [
|
||||
("kitty keyboard flags (\\e[?u)", b"\x1b[?u", rb"\x1b\[\?(\d+)u", None),
|
||||
("modifyOtherKeys (XTQMODKEYS)", b"\x1b[?4m", rb"\x1b\[>4;(\d+)m", None),
|
||||
("background color (OSC 11)", b"\x1b]11;?\x1b\\", rb"\x1b\]11;([^\x07\x1b]+)", None),
|
||||
] + [
|
||||
(
|
||||
f"DEC mode {num} — {desc}",
|
||||
b"\x1b[?%d$p" % num,
|
||||
rb"\x1b\[\?%d;(\d+)\$y" % num,
|
||||
DECRQM_VALUES,
|
||||
)
|
||||
for num, desc in DEC_MODES
|
||||
]
|
||||
|
||||
|
||||
def read_for(fd, seconds):
|
||||
buf = b""
|
||||
end = time.monotonic() + seconds
|
||||
while True:
|
||||
remaining = end - time.monotonic()
|
||||
if remaining <= 0:
|
||||
break
|
||||
r, _, _ = select.select([fd], [], [], remaining)
|
||||
if not r:
|
||||
break
|
||||
buf += os.read(fd, 4096)
|
||||
return buf
|
||||
|
||||
|
||||
def report(fd):
|
||||
lines = []
|
||||
raw_dump = b""
|
||||
for label, query, pattern, value_names in QUERIES:
|
||||
raw_dump += read_for(fd, 0.02) # drain stragglers
|
||||
start = time.monotonic()
|
||||
os.write(fd, query)
|
||||
buf = b""
|
||||
match = None
|
||||
deadline = time.monotonic() + 0.35
|
||||
while time.monotonic() < deadline:
|
||||
buf += read_for(fd, 0.05)
|
||||
match = re.search(pattern, buf)
|
||||
if match:
|
||||
break
|
||||
raw_dump += buf
|
||||
if match:
|
||||
latency = (time.monotonic() - start) * 1000
|
||||
value = match.group(1).decode("ascii", "replace")
|
||||
if value_names:
|
||||
value = f"{value} ({value_names.get(value, '?')})"
|
||||
lines.append(f" {label:45s} = {value:24s} [{latency:6.1f} ms]")
|
||||
else:
|
||||
lines.append(f" {label:45s} = (no reply)")
|
||||
|
||||
# DA1 as a fence: every terminal answers it, so its round-trip time
|
||||
# measures the whole input path (ghostty -> zellij -> pane -> here).
|
||||
start = time.monotonic()
|
||||
os.write(fd, b"\x1b[c")
|
||||
buf = b""
|
||||
match = None
|
||||
deadline = time.monotonic() + 2.0
|
||||
while time.monotonic() < deadline:
|
||||
buf += read_for(fd, 0.05)
|
||||
match = re.search(rb"\x1b\[\?([0-9;]*)c", buf)
|
||||
if match:
|
||||
break
|
||||
raw_dump += buf
|
||||
if match:
|
||||
latency = (time.monotonic() - start) * 1000
|
||||
lines.append(
|
||||
f" {'device attributes (DA1) round-trip':45s} = "
|
||||
f"{match.group(1).decode():24s} [{latency:6.1f} ms]"
|
||||
)
|
||||
else:
|
||||
lines.append(f" {'device attributes (DA1) round-trip':45s} = (NO REPLY in 2s!)")
|
||||
lines.append(f" raw bytes received: {raw_dump!r}")
|
||||
return lines
|
||||
|
||||
|
||||
def keylog(fd):
|
||||
sys.stdout.write("keylog: capturing raw bytes from the tty. Press q to finish.\r\n")
|
||||
sys.stdout.flush()
|
||||
last = time.monotonic()
|
||||
while True:
|
||||
select.select([fd], [], [], None)
|
||||
data = os.read(fd, 4096)
|
||||
now = time.monotonic()
|
||||
delta_ms = (now - last) * 1000
|
||||
last = now
|
||||
sys.stdout.write(f" +{delta_ms:8.1f} ms {data!r} hex={data.hex(' ')}\r\n")
|
||||
sys.stdout.flush()
|
||||
if data in (b"q", b"\x03", b"\x04"):
|
||||
break
|
||||
|
||||
|
||||
def main():
|
||||
mode = sys.argv[1] if len(sys.argv) > 1 else "report"
|
||||
fd = os.open("/dev/tty", os.O_RDWR)
|
||||
old = termios.tcgetattr(fd)
|
||||
lines = None
|
||||
try:
|
||||
tty.setraw(fd)
|
||||
if mode == "report":
|
||||
lines = report(fd)
|
||||
elif mode == "keylog":
|
||||
keylog(fd)
|
||||
else:
|
||||
raise SystemExit(f"unknown mode: {mode}")
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
||||
os.close(fd)
|
||||
if lines:
|
||||
print("terminal state as seen from this pane:")
|
||||
print("\n".join(lines))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,15 @@
|
||||
# One-shot reset of terminal state a TUI may have left behind (kitty keyboard
|
||||
# flags, modifyOtherKeys, application keypad/cursor, mouse/focus reporting,
|
||||
# alternate screen, termios). fish re-enables the modes it wants at the next
|
||||
# prompt, so this is safe to run any time.
|
||||
#
|
||||
# Diagnostic value: if this cures the lag, the stuck state was below fish
|
||||
# (run lag-triage next time to find which mode). If only `exec fish` cures
|
||||
# it, the lag is inside the fish process itself.
|
||||
printf '\e[<9u\e[=0;1u'
|
||||
printf '\e[>4;0m'
|
||||
printf '\e>\e[?1l'
|
||||
printf '\e[?1000l\e[?1001l\e[?1002l\e[?1003l\e[?1005l\e[?1006l\e[?1015l\e[?1016l\e[?1004l\e[?2026l'
|
||||
printf '\e[?1049l'
|
||||
stty sane
|
||||
echo "terminal state reset — if typing still lags, run lag-triage (before exec fish!)"
|
||||
@@ -63,6 +63,7 @@ in
|
||||
git.enable = lib.mkDefault true;
|
||||
helix.enable = lib.mkDefault true;
|
||||
jujutsu.enable = lib.mkDefault true;
|
||||
lag-triage.enable = lib.mkDefault true;
|
||||
lazygit.enable = lib.mkDefault true;
|
||||
# neovim.enable = lib.mkDefault true;
|
||||
nix-index.enable = lib.mkDefault true;
|
||||
|
||||
Reference in New Issue
Block a user