#!/usr/bin/env bash # Stream one pane's `event` file and print every record in words, so that the # protocol can be watched instead of guessed at. bash and coreutils only. # # WARNING -- THIS IS NOT A PASSIVE OBSERVER # # Two things change the moment this script starts. # # While a pane's event file is open, that pane's button-2 (Exec) and button-3 # (Look) actions are REPORTED and NOT PERFORMED. Middle-clicking Del in the # tag of a watched pane will print a record here and do nothing to the pane. # (Chorded Cut and Paste are exempt and behave normally.) That suppression is # the feature -- it is what lets a script define its own tag commands -- but # while you are only watching, it makes the pane feel broken. # # And a record is consumed by whoever reads it first. If another program is # driving that pane through its event file, do not point this at the same pane: # the two readers will split the stream and both will misbehave. Watch a pane # nobody owns, or watch the script instead. # # WHAT IT DEMONSTRATES # # A record is two characters -- origin and type -- then four blank separated # decimal numbers (q0, q1, flag, text length) and the text. Uppercase types # refer to the body, lowercase to the tag; that single bit of case is the whole # addressing scheme. This script spells all of it out: `M X` prints as # "mouse exec body", and the flag bits print as the words they stand for. # # The event file is read through `cat` rather than opened by the shell. bash's # `read` buffers from a seekable fd and then seeks back to correct the file # position -- fine on a real file, silently lossy on a stream whose server # ignores offsets. `cat` reads strictly forward, and the pipe it writes into is # not seekable, so nothing can be skipped. # # FILES TOUCHED: /event (read only, but see the warning). # # USAGE: eventlog [-m mountdir] [pane-id] default id: $PARDES_PANE set -u LC_ALL=C # so ${#text} counts BYTES: event offsets are byte offsets self=${0##*/} mount=${PARDES_FS:-} if [ "${1:-}" = "-m" ]; then [ $# -ge 2 ] || { echo "$self: -m needs a directory" >&2; exit 2; } mount=$2 shift 2 fi pane=${1:-${PARDES_PANE:-}} if [ -z "$mount" ] || [ -z "$pane" ]; then cat >&2 <&2; exit 2 ;; esac ev="$mount/$pane/event" [ -r "$ev" ] || { echo "$self: cannot read $ev (no such pane?)" >&2; exit 1; } origin_word() { case $1 in E) echo "fs-write" ;; # a write to this pane's body or tag F) echo "fs-action" ;; # an action taken through another of its files K) echo "keyboard" ;; M) echo "mouse" ;; *) echo "origin?$1" ;; esac } # Uppercase = body, lowercase = tag. Nothing else distinguishes the two. type_word() { case $1 in D | I | L | X) echo "body" ;; d | i | l | x) echo "tag" ;; *) echo "?" ;; esac } action_word() { case $1 in D | d) echo "delete" ;; I | i) echo "insert" ;; L | l) echo "look" ;; # button 3 X | x) echo "exec" ;; # button 2 *) echo "type?$1" ;; esac } # The flag is a bitwise OR whose meaning depends on the type. Deletes and # inserts always carry 0, so only look and exec decode to anything. flag_words() { local t=$1 f=$2 out="" case $t in X | x) (((f & 1) != 0)) && out="$out,builtin" (((f & 2) != 0)) && out="$out,expanded(record follows)" (((f & 8) != 0)) && out="$out,chorded-arg(2 records follow)" ;; L | l) (((f & 1) != 0)) && out="$out,no-load-needed" (((f & 2) != 0)) && out="$out,expanded(record follows)" (((f & 4) != 0)) && out="$out,file-or-pane-name" ;; esac [ -n "$out" ] && printf '%s' "${out#,}" || printf -- '-' } printf '%-10s %-7s %-7s %8s %8s %-24s %s\n' ORIGIN ACTION WHERE Q0 Q1 FLAG TEXT cat -- "$ev" 2>/dev/null | while IFS= read -r line; do # Blank lines are a record terminator, not a record: skip them. This is # also what keeps the reader in step with either text layout below. [ -n "$line" ] || continue o=${line:0:1} t=${line:1:1} rest=${line:2} # shellcheck disable=SC2034 read -r q0 q1 flag n text <<<"$rest" || : q0=${q0:-0} q1=${q1:-0} flag=${flag:-0} n=${n:-0} text=${text:-} case $n in *[!0-9]*) n=0 ;; esac if [ "$n" -gt 0 ] && [ -z "$text" ]; then # The counted bytes follow the newline. IFS= read -r -N "$n" text || : elif [ "$n" -gt 0 ] && [ "${#text}" -lt "$n" ]; then # The text sat on the record line and contained a newline of its # own, which the line read above swallowed. Take the remainder. want=$((n - ${#text} - 1)) more="" [ "$want" -gt 0 ] && { IFS= read -r -N "$want" more || :; } text="$text $more" fi if [ -n "$text" ]; then shown=$(printf '%q' "$text") elif [ "$n" -gt 0 ]; then shown="(short by $n bytes: the stream ended mid-record)" else # Count 0 means "no text was sent". For a delete that is the rule; # for a look or an exec it means the text was 256 bytes or longer and # was elided, or the selection was null and an expansion follows. case $t in X | x | L | l) shown="(no text: elided, or null -- read $pane/data)" ;; *) shown="" ;; esac fi printf '%-10s %-7s %-7s %8s %8s %-24s %s\n' \ "$(origin_word "$o")" "$(action_word "$t")" "$(type_word "$t")" \ "$q0" "$q1" "$(flag_words "$t" "$flag")" "$shown" done # cat ends when the pane or the whole mount goes away. That is the editor # exiting, not a failure, so say nothing and leave with 0. exit 0