Skip to content

Instantly share code, notes, and snippets.

@ken-morel
Last active March 8, 2026 08:38
Show Gist options
  • Select an option

  • Save ken-morel/f1935adfe6301ec52d5e9e26ff146ffe to your computer and use it in GitHub Desktop.

Select an option

Save ken-morel/f1935adfe6301ec52d5e9e26ff146ffe to your computer and use it in GitHub Desktop.
vibe-coded drop-in replacement for notify-send using tmux popup windows, with actions/inline-replies
#!/usr/bin/env fish
# --- Internal Popup UI Logic ---
if set -q argv[1]; and test "$argv[1]" = --internal-popup
set -l tmp_file $argv[2]
set -l timeout $argv[3]
set -l summary $argv[4]
set -l body $argv[5]
set -l reply_prompt $argv[6]
set -l actions $argv[7..-1] # Flat list: key1, label1, key2, label2...
# Catch-all trap for Ctrl-C / sudden kills
function _tmux_notify_sig --on-signal INT --on-signal TERM --on-signal HUP
if not test -s "$tmp_file"
echo -n dismissed >"$tmp_file"
end
exit 0
end
# Build Markdown content
set -l content "# $summary"
if test -n "$body"
set content "$content\n\n$body"
end
# Display content using bat if available
if command -v bat >/dev/null
printf "%b" "$content" | bat --color=always --style=plain --paging=never -l md
else if command -v batcat >/dev/null
printf "%b" "$content" | batcat --color=always --style=plain --paging=never -l md
else
printf "%b\n" "$content"
end
echo ""
if test -n "$reply_prompt"
# Reply Mode: Capture full line of text
set -l reply
if read -P "$reply_prompt: " reply
echo -n "$reply" >"$tmp_file"
else
echo -n dismissed >"$tmp_file"
end
else if test (count $actions) -gt 0
# Interaction Mode: Show numbered list
set -l keys
set -l labels
for i in (seq 1 2 (count $actions))
set -a keys $actions[$i]
set -a labels $actions[(math $i + 1)]
end
for i in (seq (count $labels))
echo "[$i] $labels[$i]"
end
echo ""
# Flush stdin using bash because fish read might lack timeout
bash -c "read -t 0.01 -n 1000 _unused" 2>/dev/null
# Input Loop: Keep asking until valid choice or explicit dismiss
while true
set -l choice
# read returns non-zero on EOF (Ctrl-D)
if not read -n 1 -P "> " choice
echo -n dismissed >"$tmp_file"
break
end
if string match -qr '^[0-9]+$' "$choice"
if test "$choice" -ge 1; and test "$choice" -le (count $keys)
# Write key explicitly and break
echo -n "$keys[$choice]" >"$tmp_file"
break
end
end
# Ignore all other keys to prevent accidental dismissal while typing
end
else
# Passive Mode (No actions)
set -l start_time (date +%s)
set -l timeout_s 0
if test "$timeout" -gt 0
set timeout_s (math -s 0 "$timeout / 1000")
if test "$timeout_s" -le 0
set timeout_s 1
end
end
while true
if test "$timeout" -eq 0
# Always show prompt and only break on Ctrl-D
if not read -n 1 -P "> " _unused
break
end
else
set -l now (date +%s)
set -l rem (math $timeout_s - ($now - $start_time))
if test $rem -le 0
break
end
# Use bash to capture a single character with a timeout and prompt
bash -c "read -r -p '> ' -n 1 -t $rem" 2>/dev/null
set -l s $status
if test $s -gt 128
break
end # Timeout reached
if test $s -eq 1
break
end # EOF (Ctrl-D)
end
# Ignore all other keys
end
echo -n dismissed >"$tmp_file"
end
# Final safety net just in case something bypassed the writes
if not test -s "$tmp_file"
echo -n dismissed >"$tmp_file"
end
exit 0
end
# --- Main Script Logic (CLI) ---
function show_help
echo "tmux-notify 0.2.0 - A drop-in replacement for notify-send for the Tmux terminal."
echo ""
echo "USAGE:"
echo " tmux-notify [OPTIONS] <SUMMARY> [BODY]"
echo ""
echo "DESCRIPTION:"
echo " Displays a notification in a Tmux popup window at the top-right of the screen."
echo " It mimics the behavior and arguments of the standard 'notify-send' utility."
echo ""
echo "OPTIONS:"
echo " -a, --app-name=NAME Sets the application name in the popup title."
echo " -A, --action=KEY=LABEL Defines an action. KEY is printed to stdout if LABEL is selected."
echo " -I, --reply[=PROMPT] Enables inline reply mode. Prints the response to stdout."
echo " (Default prompt: 'Reply')"
echo " -t, --expire-time=TIME Timeout in milliseconds. (Default: 0)"
echo " -w, --wait Waits for the notification to be closed or an action."
echo " -h, --help Shows this help message."
echo ""
echo " Accepted but ignored notify-send arguments:"
echo " -u, --urgency=LEVEL, -i, --icon=ICON, -c, --category=TYPE, --hint=TYPE:NAME:VALUE"
exit 0
end
# 1. Parse notify-send arguments
set -l original_argv $argv
set -l options h/help 'u/urgency=' 't/expire-time=' 'i/icon=' 'a/app-name=' 'A/action=+' p/print-id 'r/replace-id=' w/wait 'hint=+' 'c/category=' 'I/reply=?'
argparse $options -- $argv; or exit 1
if set -q _flag_help
show_help
end
# If this is a fire-and-forget call (no -w, -A, or -I), relaunch with -w in the
# background and exit. This effectively queues notifications.
if not set -q _flag_w; and not set -q _flag_A; and not set -q _flag_I
set -l script_path (realpath (status filename))
set -l cmd_list
set -a cmd_list (string escape -- $script_path)
# Important: Place -w FIRST so it's always parsed by the worker's argparse
set -a cmd_list -w
for arg in $original_argv
set -a cmd_list (string escape -- "$arg")
end
set -l cmd_string (string join " " -- $cmd_list)
# Run in the background, fully detached and silenced.
fish -c "$cmd_string >/dev/null 2>&1 &; disown"
exit 0
end
# From here on, we are in a "worker" instance which will show a notification.
# We must acquire a lock to ensure only one popup is displayed at a time.
# But first, check for TMUX.
if not set -q TMUX
echo "Error: tmux-notify must be run inside a tmux session." >&2
exit 1
end
set -l LOCK_DIR /tmp/tmux-notify-$USER.lock
set -l PID_FILE "$LOCK_DIR/pid"
# Ensure lock is released even if script is interrupted
function _cleanup_lock --on-event fish_exit
if test -f "$PID_FILE"
if test (cat "$PID_FILE" 2>/dev/null) = "$fish_pid"
rm -rf "$LOCK_DIR" >/dev/null 2>&1
end
end
end
# Wait until we can acquire the lock. `mkdir` is an atomic operation.
set -l lock_start_time (date +%s)
set -l lock_timeout 30
while ! mkdir "$LOCK_DIR" >/dev/null 2>&1
if test -d "$LOCK_DIR"
if test -f "$PID_FILE"
set -l other_pid (cat "$PID_FILE" 2>/dev/null)
if test -n "$other_pid"
if not kill -0 "$other_pid" >/dev/null 2>&1
rm -rf "$LOCK_DIR" >/dev/null 2>&1
continue
end
end
else
set -l lock_age (math (date +%s) - (stat -c %Y "$LOCK_DIR" 2>/dev/null; or date +%s))
if test "$lock_age" -gt 3
rm -rf "$LOCK_DIR" >/dev/null 2>&1
continue
end
end
end
if test (math (date +%s) - $lock_start_time) -gt $lock_timeout
echo "Error: Timed out waiting for tmux-notify lock." >&2
exit 1
end
sleep 0.2
end
echo "$fish_pid" >"$PID_FILE"
# 2. Extract Summary, Body and Actions
set -l summary ""
set -l body ""
if test (count $argv) -ge 1
set summary $argv[1]
end
if test (count $argv) -ge 2
# Join everything from the second argument onwards into the body
set body (string join " " -- $argv[2..-1])
end
# Default timeout is 0 (never expire)
set -l timeout 0
if set -q _flag_t
set timeout $_flag_t
end
# Default title if app-name isn't specified
set -l title " Notification "
if set -q _flag_a
set title " $_flag_a "
end
set -l reply_prompt ""
if set -q _flag_I
set reply_prompt $_flag_I
if test -z "$reply_prompt"
set reply_prompt Reply
end
end
set -l action_args
if set -q _flag_A
for a in $_flag_A
if string match -q "*=*" -- "$a"
set -l parts (string split -m 1 "=" -- "$a")
set -a action_args $parts[1] $parts[2]
else
set -a action_args "$a" "$a"
end
end
end
# 3. Prepare Communication
set -l tmp_file (mktemp -t tmux-notify.XXXXXX)
# 4. Launch the Popup
set -l script_path (realpath (status filename))
# Safely escape quotes in arguments for the internal popup call
set -l safe_args
# We explicitly pass empty strings to ensure positional arguments 4 and 5 are always present
for arg in "$script_path" --internal-popup "$tmp_file" "$timeout" "$summary" "$body" "$reply_prompt" $action_args
set -a safe_args (string escape -- "$arg")
end
set -l tmux_cmd fish
for arg in $safe_args
set tmux_cmd "$tmux_cmd $arg"
end
# Run the command in a subshell so fish/tmux correctly handle the absolute path
tmux display-popup \
-x R -y 0 \
-w 60 -h 20 \
-b rounded \
-E -E \
-T "$title" \
"$tmux_cmd"
# 5. Polling Logic
if set -q _flag_w; or set -q _flag_A; or set -q _flag_I
while not test -s "$tmp_file"
if not test -f "$tmp_file"
break
end
sleep 0.1
end
sleep 0.05
if test -f "$tmp_file"
set -l result (cat "$tmp_file")
if test "$result" != dismissed; and test "$result" != ""
# Print result for Actions (-A) or Inline Replies (-I)
if set -q _flag_A; or set -q _flag_I
echo "$result"
end
end
rm -f "$tmp_file"
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment