Indicate and jump to tmux panes waiting for attention
Show a bar indicator whenever tmux has flagged a window, the same state that highlights the tab, and jump to it on click or with Super + Ctrl + J. Tmux hooks push the state to the shell, so nothing polls while no pane is waiting. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
98e88723ae
commit
a382be2645
@@ -79,6 +79,7 @@ GROUP_DESCRIPTIONS[style]="Global UI style controls"
|
|||||||
GROUP_DESCRIPTIONS[sudo]="Sudo configuration helpers"
|
GROUP_DESCRIPTIONS[sudo]="Sudo configuration helpers"
|
||||||
GROUP_DESCRIPTIONS[system]="System status, reboot, shutdown, logout, and lock"
|
GROUP_DESCRIPTIONS[system]="System status, reboot, shutdown, logout, and lock"
|
||||||
GROUP_DESCRIPTIONS[theme]="Theme management"
|
GROUP_DESCRIPTIONS[theme]="Theme management"
|
||||||
|
GROUP_DESCRIPTIONS[tmux]="Tmux session helpers"
|
||||||
GROUP_DESCRIPTIONS[toggle]="Toggle Omarchy features"
|
GROUP_DESCRIPTIONS[toggle]="Toggle Omarchy features"
|
||||||
GROUP_DESCRIPTIONS[transcode]="Image and video transcoding"
|
GROUP_DESCRIPTIONS[transcode]="Image and video transcoding"
|
||||||
GROUP_DESCRIPTIONS[tui]="Terminal UI launchers"
|
GROUP_DESCRIPTIONS[tui]="Terminal UI launchers"
|
||||||
|
|||||||
Executable
+104
@@ -0,0 +1,104 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Show or jump to tmux windows waiting for attention
|
||||||
|
# omarchy:args=<show [--json]|focus>
|
||||||
|
# omarchy:examples=omarchy tmux alert show | omarchy tmux alert focus
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Window names can contain the separator, so they always come last when parsed.
|
||||||
|
WINDOW_FORMAT='#{window_bell_flag}#{window_activity_flag}#{window_silence_flag}|#{session_name}|#{window_index}|#{window_name}'
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "Usage: omarchy-tmux-alert <show [--json]|focus>" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Windows tmux has flagged with a bell, activity, or silence alert, which is the
|
||||||
|
# same state that highlights the window in the tmux status bar.
|
||||||
|
alerted_windows() {
|
||||||
|
local flags session index name
|
||||||
|
|
||||||
|
tmux list-windows -a -F "$WINDOW_FORMAT" 2>/dev/null |
|
||||||
|
while IFS='|' read -r flags session index name; do
|
||||||
|
if [[ $flags == *1* ]]; then
|
||||||
|
printf '%s|%s|%s\n' "$session" "$index" "$name"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
local windows=() window session index name description=""
|
||||||
|
|
||||||
|
readarray -t windows < <(alerted_windows)
|
||||||
|
|
||||||
|
for window in "${windows[@]}"; do
|
||||||
|
IFS='|' read -r session index name <<<"$window"
|
||||||
|
[[ -n $description ]] && description+=", "
|
||||||
|
description+="$name ($session:$index)"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ${1:-} == "--json" ]]; then
|
||||||
|
jq -nc --argjson count "${#windows[@]}" --arg tooltip "$description" '{count: $count, tooltip: $tooltip}'
|
||||||
|
elif [[ -n $description ]]; then
|
||||||
|
echo "$description"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
focus_terminal_window() {
|
||||||
|
local pid=$1 address="" window_pid window_address
|
||||||
|
local -A windows=()
|
||||||
|
|
||||||
|
while read -r window_pid window_address; do
|
||||||
|
windows[$window_pid]=$window_address
|
||||||
|
done < <(hyprctl clients -j | jq -r '.[] | "\(.pid) \(.address)"')
|
||||||
|
|
||||||
|
# The tmux client runs somewhere below the terminal that owns the window.
|
||||||
|
while [[ -n $pid ]] && (( pid > 1 )); do
|
||||||
|
if [[ -n ${windows[$pid]:-} ]]; then
|
||||||
|
address=${windows[$pid]}
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
pid=$(awk '/^PPid:/ { print $2 }' "/proc/$pid/status" 2>/dev/null)
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n $address ]] || return 1
|
||||||
|
|
||||||
|
hyprctl dispatch "hl.dsp.focus({ window = \"address:$address\" })" >/dev/null 2>&1 ||
|
||||||
|
hyprctl dispatch focuswindow "address:$address" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
focus() {
|
||||||
|
local target session index client tty pid
|
||||||
|
|
||||||
|
target=$(alerted_windows | head -n1)
|
||||||
|
[[ -n $target ]] || return 0
|
||||||
|
|
||||||
|
IFS='|' read -r session index _ <<<"$target"
|
||||||
|
|
||||||
|
client=$(tmux list-clients -t "$session" -F '#{client_tty}|#{client_pid}' 2>/dev/null | head -n1)
|
||||||
|
[[ -n $client ]] || client=$(tmux list-clients -F '#{client_tty}|#{client_pid}' 2>/dev/null | head -n1)
|
||||||
|
|
||||||
|
if [[ -z $client ]]; then
|
||||||
|
tmux select-window -t "$session:$index"
|
||||||
|
exec omarchy-launch-terminal tmux attach -t "$session"
|
||||||
|
fi
|
||||||
|
|
||||||
|
IFS='|' read -r tty pid <<<"$client"
|
||||||
|
tmux switch-client -c "$tty" -t "$session:$index"
|
||||||
|
omarchy-shell -q omarchy.indicators refresh
|
||||||
|
focus_terminal_window "$pid"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
show)
|
||||||
|
shift
|
||||||
|
show "$@"
|
||||||
|
;;
|
||||||
|
focus)
|
||||||
|
focus
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -81,6 +81,13 @@ set -ag terminal-features "xterm-kitty:extkeys"
|
|||||||
set -as terminal-features ",*:clipboard"
|
set -as terminal-features ",*:clipboard"
|
||||||
set -sg escape-time 10
|
set -sg escape-time 10
|
||||||
|
|
||||||
|
# Alerts
|
||||||
|
set-hook -g alert-bell 'run-shell -b "omarchy-shell -q omarchy.indicators refresh"'
|
||||||
|
set-hook -g alert-activity 'run-shell -b "omarchy-shell -q omarchy.indicators refresh"'
|
||||||
|
set-hook -g alert-silence 'run-shell -b "omarchy-shell -q omarchy.indicators refresh"'
|
||||||
|
set-hook -g after-select-window 'run-shell -b "omarchy-shell -q omarchy.indicators refresh"'
|
||||||
|
set-hook -g client-session-changed 'run-shell -b "omarchy-shell -q omarchy.indicators refresh"'
|
||||||
|
|
||||||
# Status bar
|
# Status bar
|
||||||
set -g status-position top
|
set -g status-position top
|
||||||
set -g status-interval 5
|
set -g status-interval 5
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ o.bind("SUPER + ESCAPE", "System menu", "omarchy-menu toggle system")
|
|||||||
o.bind("XF86PowerOff", "Power menu", "omarchy-menu toggle system", { locked = true })
|
o.bind("XF86PowerOff", "Power menu", "omarchy-menu toggle system", { locked = true })
|
||||||
o.bind("SUPER + K", "Show key bindings", "omarchy-menu-keybindings")
|
o.bind("SUPER + K", "Show key bindings", "omarchy-menu-keybindings")
|
||||||
o.bind("SUPER + ALT + K", "Show Tmux key bindings", "omarchy-menu-tmux-keybindings")
|
o.bind("SUPER + ALT + K", "Show Tmux key bindings", "omarchy-menu-tmux-keybindings")
|
||||||
|
o.bind("SUPER + CTRL + J", "Jump to waiting Tmux pane", "omarchy-tmux-alert focus")
|
||||||
o.bind("XF86Calculator", "Calculator", "gnome-calculator")
|
o.bind("XF86Calculator", "Calculator", "gnome-calculator")
|
||||||
|
|
||||||
o.bind_toggle("SUPER + SHIFT + SPACE", "Toggle top bar", "bar")
|
o.bind_toggle("SUPER + SHIFT + SPACE", "Toggle top bar", "bar")
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
echo "Add tmux hooks that surface waiting windows in the bar"
|
||||||
|
|
||||||
|
tmux_config="$HOME/.config/tmux/tmux.conf"
|
||||||
|
|
||||||
|
if [[ -f $tmux_config ]] && ! grep -q 'alert-bell' "$tmux_config"; then
|
||||||
|
cat >>"$tmux_config" <<'EOF'
|
||||||
|
|
||||||
|
# Alerts
|
||||||
|
set-hook -g alert-bell 'run-shell -b "omarchy-shell -q omarchy.indicators refresh"'
|
||||||
|
set-hook -g alert-activity 'run-shell -b "omarchy-shell -q omarchy.indicators refresh"'
|
||||||
|
set-hook -g alert-silence 'run-shell -b "omarchy-shell -q omarchy.indicators refresh"'
|
||||||
|
set-hook -g after-select-window 'run-shell -b "omarchy-shell -q omarchy.indicators refresh"'
|
||||||
|
set-hook -g client-session-changed 'run-shell -b "omarchy-shell -q omarchy.indicators refresh"'
|
||||||
|
EOF
|
||||||
|
|
||||||
|
omarchy-restart-tmux
|
||||||
|
fi
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Ui
|
||||||
|
|
||||||
|
BarIndicator {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property int waitingCount: 0
|
||||||
|
property string tooltip: ""
|
||||||
|
|
||||||
|
active: waitingCount > 0
|
||||||
|
activeText: ""
|
||||||
|
inactiveText: ""
|
||||||
|
activeTooltipText: tooltip
|
||||||
|
inactiveTooltipText: "No terminal is waiting"
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
if (!statusProc.running) statusProc.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(raw) {
|
||||||
|
var data = extractData(raw)
|
||||||
|
waitingCount = Number(data.count || 0)
|
||||||
|
tooltip = String(data.tooltip || "")
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: refresh()
|
||||||
|
|
||||||
|
// Alerts can also disappear without a hook, such as when the window or its
|
||||||
|
// session is killed while still flagged.
|
||||||
|
Timer {
|
||||||
|
interval: 5000
|
||||||
|
running: root.active
|
||||||
|
repeat: true
|
||||||
|
onTriggered: root.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root.indicatorHost
|
||||||
|
ignoreUnknownSignals: true
|
||||||
|
function onRefreshRequested() { root.refresh() }
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: statusProc
|
||||||
|
command: ["omarchy-tmux-alert", "show", "--json"]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
waitForEnd: true
|
||||||
|
onStreamFinished: root.update(text)
|
||||||
|
}
|
||||||
|
onExited: function(exitCode) {
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
root.waitingCount = 0
|
||||||
|
root.tooltip = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPressed: function() {
|
||||||
|
Quickshell.execDetached(["omarchy-tmux-alert", "focus"])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,6 +36,11 @@
|
|||||||
"label": "Reminder",
|
"label": "Reminder",
|
||||||
"description": "Queued reminder status"
|
"description": "Queued reminder status"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"value": "TmuxAlert",
|
||||||
|
"label": "Tmux alert",
|
||||||
|
"description": "Tmux windows waiting for attention"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"value": "NightLight",
|
"value": "NightLight",
|
||||||
"label": "Night light",
|
"label": "Night light",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ BarWidget {
|
|||||||
id: root
|
id: root
|
||||||
moduleName: "omarchy.indicators"
|
moduleName: "omarchy.indicators"
|
||||||
|
|
||||||
readonly property var defaultIndicatorEntries: [ "Dnd", "Reminder", "NightLight", "StayAwake", "ScreenRecording", "Dictation" ]
|
readonly property var defaultIndicatorEntries: [ "Dnd", "Reminder", "TmuxAlert", "NightLight", "StayAwake", "ScreenRecording", "Dictation" ]
|
||||||
readonly property var indicatorEntries: indicatorEntriesFromSettings(settings)
|
readonly property var indicatorEntries: indicatorEntriesFromSettings(settings)
|
||||||
property var activeIndicatorIds: []
|
property var activeIndicatorIds: []
|
||||||
property var indicatorActiveStates: ({})
|
property var indicatorActiveStates: ({})
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
source "$(dirname "$0")/base-test.sh"
|
||||||
|
|
||||||
|
require_command jq
|
||||||
|
|
||||||
|
test_dir=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$test_dir"' EXIT
|
||||||
|
|
||||||
|
cat >"$test_dir/tmux" <<'STUB'
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [[ $1 == "list-windows" ]]; then
|
||||||
|
cat "$TMUX_STUB_WINDOWS"
|
||||||
|
fi
|
||||||
|
STUB
|
||||||
|
chmod +x "$test_dir/tmux"
|
||||||
|
|
||||||
|
export TMUX_STUB_WINDOWS="$test_dir/windows"
|
||||||
|
export PATH="$test_dir:$PATH"
|
||||||
|
|
||||||
|
cat >"$TMUX_STUB_WINDOWS" <<'WINDOWS'
|
||||||
|
000|Work|1|editor
|
||||||
|
100|Work|2|claude
|
||||||
|
001|Side|1|server|with|pipes
|
||||||
|
WINDOWS
|
||||||
|
|
||||||
|
output=$("$ROOT/bin/omarchy-tmux-alert" show --json)
|
||||||
|
expected='{"count":2,"tooltip":"claude (Work:2), server|with|pipes (Side:1)"}'
|
||||||
|
[[ $output == "$expected" ]] || fail "tmux alert reports alerted windows" "expected: $expected"$'\n'"actual: $output"
|
||||||
|
pass "tmux alert reports alerted windows"
|
||||||
|
|
||||||
|
output=$("$ROOT/bin/omarchy-tmux-alert" show)
|
||||||
|
[[ $output == "claude (Work:2), server|with|pipes (Side:1)" ]] || fail "tmux alert describes alerted windows" "$output"
|
||||||
|
pass "tmux alert describes alerted windows"
|
||||||
|
|
||||||
|
cat >"$TMUX_STUB_WINDOWS" <<'WINDOWS'
|
||||||
|
000|Work|1|editor
|
||||||
|
WINDOWS
|
||||||
|
|
||||||
|
output=$("$ROOT/bin/omarchy-tmux-alert" show --json)
|
||||||
|
[[ $output == '{"count":0,"tooltip":""}' ]] || fail "tmux alert reports no alerted windows" "$output"
|
||||||
|
pass "tmux alert reports no alerted windows"
|
||||||
|
|
||||||
|
[[ -z $("$ROOT/bin/omarchy-tmux-alert" show) ]] || fail "tmux alert stays quiet without alerts"
|
||||||
|
pass "tmux alert stays quiet without alerts"
|
||||||
|
|
||||||
|
: >"$TMUX_STUB_WINDOWS"
|
||||||
|
output=$("$ROOT/bin/omarchy-tmux-alert" show --json)
|
||||||
|
[[ $output == '{"count":0,"tooltip":""}' ]] || fail "tmux alert handles a missing tmux server" "$output"
|
||||||
|
pass "tmux alert handles a missing tmux server"
|
||||||
Reference in New Issue
Block a user