Skip to content

Instantly share code, notes, and snippets.

@Chinoman10
Last active April 16, 2026 14:15
Show Gist options
  • Select an option

  • Save Chinoman10/e7dc607b5e2db9f46f087916fcee1fc0 to your computer and use it in GitHub Desktop.

Select an option

Save Chinoman10/e7dc607b5e2db9f46f087916fcee1fc0 to your computer and use it in GitHub Desktop.
notmynet, a rolling "network monitor" to view at a glance how stable your internet connection is (by pinging Cloudflare's DNS servers)
#!/usr/bin/env bash
set -u
# =========================
# Defaults
# Precedence: defaults < env vars < CLI args
# =========================
TARGET="${TARGET:-1.1.1.1}"
WINDOW="${WINDOW:-60}"
INTERVAL="${INTERVAL:-1}" # seconds between tests; may be fractional
PING_TIMEOUT="${PING_TIMEOUT:-1}" # whole seconds, for portability
FAST_MS="${FAST_MS:-20}" # < FAST_MS => green
SLOW_MS="${SLOW_MS:-150}" # FAST_MS..(SLOW_MS-1) => yellow, >= SLOW_MS => orange
USE_COLOR=1
SHOW_LEGEND=1
ASCII_MODE=0
COMPACT=0
# =========================
# UI
# =========================
RESET=$'\033[0m'
DIM=$'\033[2m'
GREEN=$'\033[32m'
YELLOW=$'\033[33m'
RED=$'\033[31m'
ORANGE=$'\033[38;5;208m'
BLOCK="█"
EMPTY_BLOCK="░"
declare -a results=() # 1 = success, 0 = fail
declare -a rtts=() # RTT in ms for success, empty for fail
streak_state=""
streak_len=0
outage_started_at=0
die() {
echo "Error: $*" >&2
exit 1
}
print_help() {
cat <<'EOF'
notmynet.sh - rolling internet connectivity monitor
Usage:
notmynet.sh [options] [target]
Target:
target Ping target as a positional argument
Default: 1.1.1.1
Options:
-t, --target HOST Ping target host/IP
-w, --window N Rolling window size
-i, --interval SEC Seconds between tests (supports decimals, e.g. 0.5)
-o, --timeout SEC Ping timeout in whole seconds
-f, --fast-ms MS Fast latency threshold
-s, --slow-ms MS Slow latency threshold
-a, --ascii Use ASCII-friendly characters instead of solid blocks
-c, --compact One-line compact display
--no-color Disable ANSI colors
--no-legend Hide the color legend
-h, --help Show this help
Color rules:
green RTT < FAST_MS
yellow FAST_MS <= RTT < SLOW_MS
orange RTT >= SLOW_MS
red failed / timed out
Environment variables:
TARGET WINDOW INTERVAL PING_TIMEOUT FAST_MS SLOW_MS
Precedence:
defaults < environment < CLI args
Examples:
notmynet.sh
notmynet.sh 8.8.8.8
notmynet.sh --target 9.9.9.9 --window 60 --interval 2
notmynet.sh --compact
WINDOW=40 INTERVAL=0.5 FAST_MS=10 SLOW_MS=80 notmynet.sh
notmynet.sh --ascii --no-color
Notes:
- INTERVAL may be fractional.
- PING_TIMEOUT is kept as whole seconds for portability across Linux/macOS.
- Ctrl+C exits cleanly.
EOF
}
is_uint() {
[[ "$1" =~ ^[0-9]+$ ]]
}
is_number() {
[[ "$1" =~ ^[0-9]+([.][0-9]+)?$ ]]
}
num_ge_zero() {
awk -v x="$1" 'BEGIN { exit !(x >= 0) }'
}
float_lt() {
awk -v a="$1" -v b="$2" 'BEGIN { exit !(a < b) }'
}
cleanup() {
if (( COMPACT )); then
printf '\r\033[2K'
else
printf '\n'
fi
printf '%s' "$RESET"
tput cnorm 2>/dev/null || true
}
handle_signal() {
trap - EXIT
cleanup
exit 130
}
trap cleanup EXIT
trap handle_signal INT TERM
format_duration() {
local total=$1
local h=$(( total / 3600 ))
local m=$(( (total % 3600) / 60 ))
local s=$(( total % 60 ))
printf '%02d:%02d:%02d' "$h" "$m" "$s"
}
upper() {
printf '%s' "$1" | tr '[:lower:]' '[:upper:]'
}
color_for_rtt() {
local rtt="$1"
local rtt_int="${rtt%.*}"
[[ -z "$rtt_int" ]] && rtt_int=0
if (( rtt_int < FAST_MS )); then
printf '%s' "$GREEN"
elif (( rtt_int < SLOW_MS )); then
printf '%s' "$YELLOW"
else
printf '%s' "$ORANGE"
fi
}
paint() {
local color="$1"
local text="$2"
if (( USE_COLOR )); then
printf '%b%s%b' "$color" "$text" "$RESET"
else
printf '%s' "$text"
fi
}
build_legend() {
printf 'Legend: '
paint "$GREEN" "$BLOCK"
printf ' < %sms ' "$FAST_MS"
paint "$YELLOW" "$BLOCK"
printf ' %s to <%sms ' "$FAST_MS" "$SLOW_MS"
paint "$ORANGE" "$BLOCK"
printf ' >= %sms ' "$SLOW_MS"
paint "$RED" "$BLOCK"
printf ' fail/timeout'
}
disable_colors() {
RESET=''
DIM=''
GREEN=''
YELLOW=''
RED=''
ORANGE=''
}
# =========================
# Parse CLI args
# =========================
POSITIONAL_TARGET=""
while (( $# > 0 )); do
case "$1" in
-t|--target)
(( $# >= 2 )) || die "$1 requires a value"
TARGET="$2"
shift 2
;;
-w|--window)
(( $# >= 2 )) || die "$1 requires a value"
WINDOW="$2"
shift 2
;;
-i|--interval)
(( $# >= 2 )) || die "$1 requires a value"
INTERVAL="$2"
shift 2
;;
-o|--timeout)
(( $# >= 2 )) || die "$1 requires a value"
PING_TIMEOUT="$2"
shift 2
;;
-f|--fast-ms)
(( $# >= 2 )) || die "$1 requires a value"
FAST_MS="$2"
shift 2
;;
-s|--slow-ms)
(( $# >= 2 )) || die "$1 requires a value"
SLOW_MS="$2"
shift 2
;;
-a|--ascii)
ASCII_MODE=1
shift
;;
-c|--compact)
COMPACT=1
shift
;;
--no-color)
USE_COLOR=0
shift
;;
--no-legend)
SHOW_LEGEND=0
shift
;;
-h|--help)
print_help
exit 0
;;
--)
shift
break
;;
-*)
die "unknown option: $1"
;;
*)
if [[ -n "$POSITIONAL_TARGET" ]]; then
die "only one positional target is allowed"
fi
POSITIONAL_TARGET="$1"
shift
;;
esac
done
if [[ -n "$POSITIONAL_TARGET" ]]; then
TARGET="$POSITIONAL_TARGET"
fi
# =========================
# Validation
# =========================
is_uint "$WINDOW" || die "WINDOW must be a positive integer"
is_number "$INTERVAL" || die "INTERVAL must be a number >= 0"
is_uint "$PING_TIMEOUT" || die "PING_TIMEOUT must be a positive integer"
is_uint "$FAST_MS" || die "FAST_MS must be a non-negative integer"
is_uint "$SLOW_MS" || die "SLOW_MS must be a non-negative integer"
(( WINDOW > 0 )) || die "WINDOW must be > 0"
num_ge_zero "$INTERVAL" || die "INTERVAL must be >= 0"
(( PING_TIMEOUT > 0 )) || die "PING_TIMEOUT must be > 0"
float_lt "$FAST_MS" "$SLOW_MS" || die "FAST_MS must be less than SLOW_MS"
if (( ASCII_MODE )); then
BLOCK="#"
EMPTY_BLOCK="."
fi
if (( COMPACT )); then
SHOW_LEGEND=0
fi
if (( ! USE_COLOR )) || [[ ! -t 1 ]] || [[ -n "${NO_COLOR:-}" ]]; then
USE_COLOR=0
disable_colors
fi
# =========================
# ping wrapper
# macOS and Linux use different units for ping -W
# =========================
case "$(uname -s)" in
Darwin)
ping_cmd() {
ping -n -c 1 -W "$(( PING_TIMEOUT * 1000 ))" "$TARGET" 2>/dev/null
}
;;
*)
ping_cmd() {
ping -n -c 1 -W "$PING_TIMEOUT" "$TARGET" 2>/dev/null
}
;;
esac
tput civis 2>/dev/null || true
if (( ! COMPACT )); then
printf '\033[2J'
fi
while true; do
output="$(ping_cmd)"
rc=$?
current_state="down"
current_rtt=""
last_ping_text="$(paint "$RED" "timeout")"
if [[ $rc -eq 0 ]]; then
current_rtt="$(sed -n 's/.*time=\([0-9.][0-9.]*\).*/\1/p' <<< "$output" | head -n1)"
[[ -z "$current_rtt" ]] && current_rtt="0"
results+=(1)
rtts+=("$current_rtt")
current_state="up"
current_color="$(color_for_rtt "$current_rtt")"
last_ping_text="$(paint "$current_color" "${current_rtt}ms")"
else
results+=(0)
rtts+=("")
fi
if (( ${#results[@]} > WINDOW )); then
results=("${results[@]:1}")
rtts=("${rtts[@]:1}")
fi
if [[ "$current_state" == "$streak_state" ]]; then
((streak_len++))
else
streak_state="$current_state"
streak_len=1
fi
now_epoch="$(date +%s)"
if [[ "$current_state" == "down" ]]; then
if (( outage_started_at == 0 )); then
outage_started_at=$now_epoch
fi
outage_timer="$(format_duration $(( now_epoch - outage_started_at )))"
else
outage_started_at=0
outage_timer="--:--:--"
fi
total=${#results[@]}
fail_count=0
ok_count=0
sum_rtt=0
worst_rtt=""
graph=""
for i in "${!results[@]}"; do
if [[ "${results[$i]}" -eq 1 ]]; then
rtt="${rtts[$i]}"
block_color="$(color_for_rtt "$rtt")"
graph+="$(paint "$block_color" "$BLOCK")"
((ok_count++))
sum_rtt="$(awk -v a="$sum_rtt" -v b="$rtt" 'BEGIN { printf "%.3f", a + b }')"
if [[ -z "$worst_rtt" ]]; then
worst_rtt="$rtt"
else
if awk -v a="$rtt" -v b="$worst_rtt" 'BEGIN { exit !(a > b) }'; then
worst_rtt="$rtt"
fi
fi
else
graph+="$(paint "$RED" "$BLOCK")"
((fail_count++))
fi
done
for ((i=total; i<WINDOW; i++)); do
graph+="$(paint "$DIM" "$EMPTY_BLOCK")"
done
if (( ok_count > 0 )); then
avg_rtt="$(awk -v s="$sum_rtt" -v n="$ok_count" 'BEGIN { printf "%.1f", s / n }')"
worst_color="$(color_for_rtt "$worst_rtt")"
worst_ping_text="$(paint "$worst_color" "${worst_rtt}ms")"
else
avg_rtt="N/A"
worst_ping_text="N/A"
fi
fail_pct="$(awk -v f="$fail_count" -v t="$total" 'BEGIN {
if (t == 0) printf "0.0";
else printf "%.1f", (f / t) * 100
}')"
if [[ "$current_state" == "up" ]]; then
status_text="$(paint "$GREEN" "UP")"
streak_text="$(paint "$GREEN" "$(upper "$streak_state"):$streak_len")"
else
status_text="$(paint "$RED" "DOWN")"
streak_text="$(paint "$RED" "$(upper "$streak_state"):$streak_len")"
fi
if (( COMPACT )); then
printf '\r\033[2K'
printf "%s %b %b last:%b avg:%sms worst:%b fail:%s%% streak:%b outage:%s" \
"$TARGET" "$graph" "$status_text" "$last_ping_text" "$avg_rtt" "$worst_ping_text" \
"$fail_pct" "$streak_text" "$outage_timer"
else
printf '\033[H\033[J'
printf "Internet monitor -> %s\n" "$TARGET"
printf "Window (%d): %b\n" "$WINDOW" "$graph"
if (( SHOW_LEGEND )); then
build_legend
printf '\n'
fi
printf "Status: %b Last ping: %b\n" "$status_text" "$last_ping_text"
printf "Streak: %b Outage: %s\n" "$streak_text" "$outage_timer"
printf "Average ping: %s ms Worst ping: %b Failed: %s%% (%d/%d)\n" \
"$avg_rtt" "$worst_ping_text" "$fail_pct" "$fail_count" "$total"
fi
sleep "$INTERVAL"
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment