Last active
April 16, 2026 14:15
-
-
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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