tmux statusline: directly-held WAN/LAN IP, offline ✕, native window-status

- tmux-ip: show the first directly-held public/WAN IP on a non-tunnel interface,
  else the LAN/private IP. No external 'what's my IP' lookup and no DigitalOcean
  special-case (a droplet's eth0 public IP is found by the same interface scan);
  RFC1918/link-local/CGNAT classed as not-WAN, so a NAT'd box shows its LAN IP.
- tmux-net: show ✕ when there's no local IP and no VPN tunnel (offline). VPN ⇡
  overlay unchanged.
- .tmux.conf: window-status icon + path now native tmux formats (no per-redraw
  #() forks); tmux-window-icon kept as fallback.
This commit is contained in:
2026-06-30 16:38:06 -07:00
parent c32dbb92ac
commit 0eb605a835
3 changed files with 46 additions and 23 deletions
+40 -20
View File
@@ -1,34 +1,53 @@
#!/bin/bash
#
# Print the "best" IP for the tmux status bar.
# Preference: DO reserved/floating IP > public egress (ipify) > first local IP.
#
# Shows a directly-held public/WAN IP when the machine has one on an interface,
# otherwise its LAN/private IP. No external "what's my IP" lookup -- a NAT'd box
# shows its LAN IP, never the gateway's egress. Works the same everywhere: a box
# with a public IP on a NIC (incl. a cloud droplet's eth0) shows that WAN IP; a
# NAT'd box shows its LAN IP. The VPN overlay is added separately by tmux-net via
# tmux-vpn-ip and is untouched here.
#
set -u
META="http://169.254.169.254/metadata/v1"
CURL="curl -sf --connect-timeout 1 --max-time 2"
is_digitalocean() {
[ -r /sys/class/dmi/id/sys_vendor ] \
&& grep -qi digitalocean /sys/class/dmi/id/sys_vendor
# This machine's own non-loopback IPv4 addresses, one per line. VPN tunnels are
# excluded (tmux-net surfaces the VPN address separately as the ⇡ segment).
list_ipv4() {
if command -v ip >/dev/null 2>&1; then
ip -4 -o addr show scope global 2>/dev/null \
| awk '$2 !~ /^(tun|wg|ppp|tap)/ {print $4}' | cut -d/ -f1
elif hostname -I >/dev/null 2>&1; then
hostname -I 2>/dev/null | tr ' ' '\n'
else
ifconfig 2>/dev/null | awk '/inet /{print $2}'
fi | grep -E '^([0-9]{1,3}\.){3}[0-9]{1,3}$' | grep -v '^127\.'
}
do_anchor_ip() {
local kind active ip
for kind in reserved_ip floating_ip; do
active=$($CURL "$META/$kind/ipv4/active" 2>/dev/null) || continue
[ "$active" = "true" ] || continue
ip=$($CURL "$META/$kind/ipv4/ip_address" 2>/dev/null) || continue
[ -n "$ip" ] && { printf '%s\n' "$ip"; return 0; }
done
# RFC1918 / link-local / CGNAT -- not a directly-routable WAN address.
is_private_ipv4() {
case "$1" in
10.*|192.168.*|169.254.*) return 0 ;;
172.1[6-9].*|172.2[0-9].*|172.3[01].*) return 0 ;;
100.6[4-9].*|100.[7-9][0-9].*|100.1[01][0-9].*|100.12[0-7].*) return 0 ;;
*) return 1 ;;
esac
}
# First directly-held public/WAN IP, if the machine has one on an interface.
public_ip() {
local ip
while IFS= read -r ip; do
[ -n "$ip" ] || continue
is_private_ipv4 "$ip" || { printf '%s\n' "$ip"; return 0; }
done <<EOF
$(list_ipv4)
EOF
return 1
}
if is_digitalocean && do_anchor_ip; then
exit 0
fi
# Fallback: the machine's primary interface IP (covers macOS and odd setups).
print_local_ip() {
local iface ip
if command -v ipconfig >/dev/null 2>&1; then
@@ -44,6 +63,7 @@ print_local_ip() {
ifconfig 2>/dev/null | awk '/inet /{ if ($2 != "127.0.0.1") { print $2; exit } }'
}
ip=$(curl -s --connect-timeout 3 https://api.ipify.org 2>/dev/null || true)
# A directly-held public/WAN IP if the box has one, else the LAN IP.
ip=$(public_ip || true)
[ -z "${ip:-}" ] && ip=$(print_local_ip)
printf '%s\n' "${ip:-}"
+4 -1
View File
@@ -3,6 +3,7 @@
# Emit the tmux status-bar network segment.
# No VPN: "⌂ <local_ip>"
# VPN up: "⌂ <local_ip> / ⇡ <vpn_ip>"
# Offline: "✕" (no local IP and no VPN tunnel)
#
set -u
@@ -10,7 +11,9 @@ set -u
local_ip=$("$HOME/.local/bin/tmux-ip" 2>/dev/null || true)
vpn_ip=$("$HOME/.local/bin/tmux-vpn-ip" 2>/dev/null || true)
if [ -n "${vpn_ip:-}" ]; then
if [ -z "${local_ip:-}" ] && [ -z "${vpn_ip:-}" ]; then
printf '\xe2\x9c\x95\n' # ✕ no active connection
elif [ -n "${vpn_ip:-}" ]; then
printf '\xe2\x8c\x82 %s / \xe2\x87\xa1 %s\n' "${local_ip:-}" "$vpn_ip"
else
printf '\xe2\x8c\x82 %s\n' "${local_ip:-}"
+2 -2
View File
@@ -80,8 +80,8 @@ set -g pane-active-border-style fg=colour39
set -g message-style fg=colour16,bg=colour221,bold
set -g status-left '#[fg=colour235,bg=colour252,bold] ❐ #S #[fg=colour252,bg=colour238,nobold]#[fg=colour245,bg=colour238,bold] #(whoami) '
set -g status-right '#[bold][#[nobold,fg=colour229]#h#[fg=default] / #[fg=colour229]#(~/.local/bin/tmux-net)#[fg=default,bold]]#[nobold,fg=colour255] %-I:%M%P %d-%b-%Y '
set -g window-status-format '#[fg=colour235,bg=colour252,nobold] #(~/.local/bin/tmux-window-icon #{window_index}) #(pwd="#{pane_current_path}"; echo ${pwd####*/}) #W '
set -g window-status-current-format '#[fg=colour234,bg=colour39,bold] [#[fg=colour232,bold]#{?window_zoomed_flag,#[fg=colour228],} #(~/.local/bin/tmux-window-icon #{window_index}) #(pwd="#{pane_current_path}"; echo ${pwd####*/}) #W #[fg=colour234,bold]] '
set -g window-status-format '#[fg=colour235,bg=colour252,nobold] #{?#{==:#{window_index},1},❶,#{?#{==:#{window_index},2},❷,#{?#{==:#{window_index},3},❸,#{?#{==:#{window_index},4},❹,#{?#{==:#{window_index},5},❺,#{?#{==:#{window_index},6},❻,#{?#{==:#{window_index},7},❼,#{?#{==:#{window_index},8},❽,#{?#{==:#{window_index},9},❾,#{?#{==:#{window_index},10},❿,#{window_index}}}}}}}}}}} #{b:pane_current_path} #W '
set -g window-status-current-format '#[fg=colour234,bg=colour39,bold] [#[fg=colour232,bold]#{?window_zoomed_flag,#[fg=colour228],} #{?#{==:#{window_index},1},❶,#{?#{==:#{window_index},2},❷,#{?#{==:#{window_index},3},❸,#{?#{==:#{window_index},4},❹,#{?#{==:#{window_index},5},❺,#{?#{==:#{window_index},6},❻,#{?#{==:#{window_index},7},❼,#{?#{==:#{window_index},8},❽,#{?#{==:#{window_index},9},❾,#{?#{==:#{window_index},10},❿,#{window_index}}}}}}}}}}} #{b:pane_current_path} #W #[fg=colour234,bold]] '
set-option -g status-interval 60
# Patch for OS X pbpaste and pbcopy under tmux.