Skip to content

Commit

Permalink
feat: updated netspeed widget (#72)
Browse files Browse the repository at this point in the history
## Minor changes

- [X] netspeed: auto-detect interface if undefined
- [X] netspeed: new widget icons for device class and state
- [X] netspeed: detect interface type (wired/wireless)
- [X] netspeed: detect interface online state
- [X] netspeed: show IPv4 address with the `@tokyo-night-tmux_netspeed_showip` option
- [X] netspeed: vary the refresh rate with the `@tokyo-night-tmux_netspeed_refresh` option

## Bug & security fixes

- [X] netspeed: improve accuracy and performance of netspeed calculations with `bc`
- [X] netspeed: handle 
- [X] netspeed: handle VPN devices on macOS using the primary interface
  • Loading branch information
Stealthii authored Apr 22, 2024
1 parent 16a4ef5 commit 2bc27a9
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 56 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,13 @@ set -g @tokyo-night-tmux_show_music 1
```

#### Netspeed widget
![Snap netspeed](snaps/netspeed.png)

```bash
set -g @tokyo-night-tmux_show_netspeed 1
set -g @tokyo-night-tmux_netspeed_iface "wlan0" # find your interface with ip link
set -g @tokyo-night-tmux_netspeed_iface "wlan0" # Detected via default route
set -g @tokyo-night-tmux_netspeed_showip 1 # Display IPv4 address (default 0)
set -g @tokyo-night-tmux_netspeed_refresh 1 # Update interval in seconds (default 1)
```

#### Path Widget
Expand Down
66 changes: 66 additions & 0 deletions lib/netspeed.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/usr/bin/env bash

# Get network transmit data
function get_bytes() {
local interface="$1"
if [[ "$(uname)" == "Linux" ]]; then
awk -v interface="$interface" '$1 == interface ":" {print $2, $10}' /proc/net/dev
elif [[ "$(uname)" == "Darwin" ]]; then
netstat -ib | awk -v interface="$interface" '/^'"${interface}"'/ {print $7, $10}' | head -n1
else
# Unsupported operating system
exit 1
fi
}

# Convert into readable format
function readable_format() {
local bytes=$1
local secs=${2:-1}

if [[ $bytes -lt 1048576 ]]; then
echo "$(bc -l <<<"scale=1; $bytes / 1024 / $secs")KB/s"
else
echo "$(bc -l <<<"scale=1; $bytes / 1048576 / $secs")MB/s"
fi
}

# Auto-determine interface
function find_interface() {
local interface
if [[ $(uname) == "Linux" ]]; then
interface=$(awk '$2 == 00000000 {print $1}' /proc/net/route)
elif [[ $(uname) == "Darwin" ]]; then
interface=$(route get default 2>/dev/null | grep interface | awk '{print $2}')
# If VPN, fallback to en0
[[ ${interface:0:4} == "utun" ]] && interface="en0"
fi
echo "$interface"
}

# Detect interface IPv4 and status
function interface_ipv4() {
local interface="$1"
local ipv4_addr
local status="up" # Default assumption
if [[ $(uname) == "Darwin" ]]; then
# Check for an IPv4 on macOS
ipv4_addr=$(ipconfig getifaddr "$interface")
[[ -z $ipv4_addr ]] && status="down"
elif [[ $(uname) == "Linux" ]]; then
# Use 'ip' command to check for IPv4 address
if command -v ip >/dev/null 2>&1; then
ipv4_addr=$(ip addr show dev "$interface" 2>/dev/null | grep "inet\b" | awk '{sub("/.*", "", $2); print $2}')
[[ -z $ipv4_addr ]] && status="down"
# Use 'ifconfig' command to check for IPv4 address
elif command -v ifconfig >/dev/null 2>&1; then
ipv4_addr=$(ifconfig "$interface" 2>/dev/null | grep "inet\b" | awk '{print $2}')
[[ -z $ipv4_addr ]] && status="down"
# Fallback to operstate on Linux
elif [[ $(cat "/sys/class/net/$interface/operstate" 2>/dev/null) != "up" ]]; then
status="down"
fi
fi
echo "$ipv4_addr"
[[ $status == "up" ]] && return 0 || return 1
}
Binary file added snaps/netspeed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
99 changes: 50 additions & 49 deletions src/netspeed.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,70 +4,71 @@
# email : [email protected]
#<------------------------------------------------------------------------------------------>

# Check if enabled
ENABLED=$(tmux show-option -gv @tokyo-night-tmux_show_netspeed 2>/dev/null)
[[ ${ENABLED} -ne 1 ]] && exit 0

# Imports
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.."
. "${ROOT_DIR}/lib/coreutils-compat.sh"

# Check the global value
SHOW_NETSPEED=$(tmux show-option -gv @tokyo-night-tmux_show_netspeed)
if [ "$SHOW_NETSPEED" != "1" ]; then
exit 0
fi

CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source $CURRENT_DIR/themes.sh
source "$ROOT_DIR/src/themes.sh"
source "$ROOT_DIR/lib/netspeed.sh"

# Get network interface
INTERFACE=$(tmux show-option -gv @tokyo-night-tmux_netspeed_iface 2>/dev/null)
# Show IP address
SHOW_IP=$(tmux show-option -gv @tokyo-night-tmux_netspeed_showip 2>/dev/null)
# Time between refresh
TIME_DIFF=$(tmux show-option -gv @tokyo-night-tmux_netspeed_refresh 2>/dev/null)
TIME_DIFF=${TIME_DIFF:-1}

# Get network transmit data from /proc/net/dev
get_bytes() {
local interface="$1"
if [[ "$(uname)" == "Linux" ]]; then
awk -v interface="$interface" '$1 == interface ":" {print $2, $10}' /proc/net/dev
elif [[ "$(uname)" == "Darwin" ]]; then
netstat -ib | awk -v interface="$interface" '/^'${interface}'/ {print $7, $10}'
else
# Unsupported operating system
exit 1
fi
}

# Convert into readable format
readable_format() {
local bytes=$1
# Icons
declare -A NET_ICONS
NET_ICONS[wifi_up]="#[fg=${THEME[foreground]}]\U000f05a9" # nf-md-wifi
NET_ICONS[wifi_down]="#[fg=${THEME[red]}]\U000f05aa" # nf-md-wifi_off
NET_ICONS[wired_up]="#[fg=${THEME[foreground]}]\U000f0318" # nf-md-lan_connect
NET_ICONS[wired_down]="#[fg=${THEME[red]}]\U000f0319" # nf-md-lan_disconnect
NET_ICONS[traffic_tx]="#[fg=${THEME[bblue]}]\U000f06f6" # nf-md-upload_network
NET_ICONS[traffic_rx]="#[fg=${THEME[bgreen]}]\U000f06f4" # nf-md-download_network
NET_ICONS[ip]="#[fg=${THEME[foreground]}]\U000f0a5f" # nf-md-ip

# Convert bytes to KBps, 'bc' is dependency, 'pacman -S bc'
local kbps=$(echo "scale=1; $bytes / 1024" | bc)
if (($(echo "$kbps < 1" | bc -l))); then
echo "0.0B"
elif (($(echo "$kbps >= 1024" | bc -l))); then
# Convert KBps to MBps
local mbps=$(echo "scale=1; $kbps / 1024" | bc)
echo "${mbps}MB/s"
else
echo "${kbps}KB/s"
fi
}
# Determine interface if not set
if [[ -z $INTERFACE ]]; then
INTERFACE=$(find_interface)
[[ -z $INTERFACE ]] && exit 1
# Update tmux option for this session
tmux set-option -g @tokyo-night-tmux_netspeed_iface "$INTERFACE"
fi

# Echo network speed
read RX1 TX1 < <(get_bytes "$INTERFACE")
sleep 1
read RX2 TX2 < <(get_bytes "$INTERFACE")
read -r RX1 TX1 < <(get_bytes "$INTERFACE")
sleep "$TIME_DIFF"
read -r RX2 TX2 < <(get_bytes "$INTERFACE")

RX_DIFF=$((RX2 - RX1))
TX_DIFF=$((TX2 - TX1))

TIME_DIFF=1
RX_SPEED="#[fg=${THEME[foreground]}]$(readable_format "$RX_DIFF" "$TIME_DIFF")"
TX_SPEED="#[fg=${THEME[foreground]}]$(readable_format "$TX_DIFF" "$TIME_DIFF")"

# Interface icon
if [[ ${INTERFACE} == "en0" ]] || [[ -d /sys/class/net/${INTERFACE}/wireless ]]; then
IFACE_TYPE="wifi"
else
IFACE_TYPE="wired"
fi

RX_SPEED=$(readable_format "$((RX_DIFF / TIME_DIFF))")
TX_SPEED=$(readable_format "$((TX_DIFF / TIME_DIFF))")
# Detect interface IPv4 and state
if IPV4_ADDR=$(interface_ipv4 "$INTERFACE"); then
IFACE_STATUS="up"
else
IFACE_STATUS="down"
fi

NETWORK_ICON="󰈀"
NETWORK_ICON=${NET_ICONS[${IFACE_TYPE}_${IFACE_STATUS}]}

# TODO: Use a more sophisticated method to detect iface type (wifi, ethernet, etc)
if [ ${INTERFACE:0:1} == "w" ]; then
NETWORK_ICON=""
OUTPUT="${RESET}${NET_ICONS[traffic_rx]} $RX_SPEED ${NET_ICONS[traffic_tx]} $TX_SPEED $NETWORK_ICON #[dim]$INTERFACE "
if [[ ${SHOW_IP} -ne 0 ]] && [[ -n $IPV4_ADDR ]]; then
OUTPUT+="${NET_ICONS[ip]} #[dim]$IPV4_ADDR "
fi

echo "${RESET}░ #[fg=${THEME[bgreen]}]󰛴${RESET} $RX_SPEED #[fg=${THEME[bblue]}]󰛶${RESET} $TX_SPEED ${NETWORK_ICON} #[dim]$INTERFACE "
echo -e "$OUTPUT"
118 changes: 118 additions & 0 deletions test/netspeed.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#!/usr/bin/env bats

setup() {
if [[ $(uname) == "Darwin" ]]; then
HOMEBREW_PREFIX=$(brew --prefix)
load "${HOMEBREW_PREFIX}/lib/bats-mock/stub.bash"
else
load /usr/lib/bats-mock/stub.bash
fi
# shellcheck source=lib/netspeed.sh
source "${BATS_TEST_DIRNAME}/../lib/netspeed.sh"

# macOS stubs
if [[ $(uname) == "Darwin" ]]; then
stub route \
"get default : echo -e ' gateway: 192.168.0.1\n interface: en0\n flags: <UP,GATEWAY,DONE,STATIC,PRCLONING,GLOBAL>'" \
"get default : echo -e ' gateway: 192.168.1.1\n interface: en1\n flags: <UP,GATEWAY,DONE,STATIC,PRCLONING,GLOBAL>'" \
"get default : echo -e ' gateway: 10.23.45.67\n interface: utun4\n flags: <UP,GATEWAY,DONE,STATIC,PRCLONING,GLOBAL>'"
stub ipconfig \
"getifaddr en0 : echo 172.17.0.2"
stub netstat \
"-ib : echo -e 'en0 1500 <Link#15> 49:2f:d3:60:03:33 79545944 0 67521187295 39466094 0 14135888609 0\nen0 1500 fake-host. fe80:f::123:beef: 79545944 - 67521187295 39466094 - 14135888609 -'"
elif [[ $(uname) == "Linux" ]]; then
stub ip \
"addr show dev eth0 : echo -e '18: eth0@if19: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 65535 qdisc noqueue state UP group default\n link/ether 49:2f:d3:60:03:33 brd ff:ff:ff:ff:ff:ff link-netnsid 0\n inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0\n valid_lft forever preferred_lft forever\n inet6 fe80::b576:50dc:d0a6:8d9a/64 scope link\n valid_lft forever preferred_lft forever'"
stub ifconfig \
"eth0 : echo -e 'eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 65535\n inet 172.17.0.2 netmask 255.255.0.0 broadcast 172.17.255.255\n inet6 fe80::b576:50dc:d0a6:8d9a prefixlen 64 scopeid 0x20<link>\n ether 49:2f:d3:60:03:33 txqueuelen 0 (Ethernet)\n RX packets 3609 bytes 11565908 (11.0 MiB)\n RX errors 0 dropped 0 overruns 0 frame 0\n TX packets 2716 bytes 182918 (178.6 KiB)\n TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0'"
fi
}

teardown() {
if [[ $(uname) == "Darwin" ]]; then
run unstub route
run unstub ipconfig
run unstub netstat
elif [[ $(uname) == "Linux" ]]; then
run unstub ip
run unstub ifconfig
fi
}

@test "Test get_bytes (macOS)" {
# Skip test if not on macOS
if [[ $(uname) != "Darwin" ]]; then
skip
fi
run get_bytes en0
[[ $output == "67521187295 14135888609" ]]
}

@test "Test readable_format" {
run readable_format 1024
[[ $output == "1.0KB/s" ]]
run readable_format 1048576
[[ $output == "1.0MB/s" ]]
run readable_format 34829287
[[ $output == "33.2MB/s" ]]
}

@test "Test readable_format with delay" {
run readable_format 34829287 1.0
[[ $output == "33.2MB/s" ]]
run readable_format 19578465 3
[[ $output == "6.2MB/s" ]]
run readable_format 29584754 2.5
[[ $output == "11.2MB/s" ]]
}

@test "Test find_interface (macOS)" {
# Skip test if not on macOS
if [[ $(uname) != "Darwin" ]]; then
skip
fi

# en0 wifi
run find_interface
[[ $output == "en0" ]]
# en1 wired
run find_interface
[[ $output == "en1" ]]
# utun4 vpn
run find_interface
[[ $output == "en0" ]] # should fallback to en0
}

@test "Test interface_ipv4 (Linux)" {
# Skip test if not on Linux
if [[ $(uname) != "Linux" ]]; then
skip
fi

# Test docker local IP
run interface_ipv4 eth0
[[ $output == "172.17.0.2" ]]
[[ $status == 0 ]]

# Test non-existing interface
run interface_ipv4 eth69
[[ $output == "" ]]
[[ $status == 1 ]]
}

@test "Test interface_ipv4 (macOS)" {
# Skip test if not on Linux
if [[ $(uname) != "Darwin" ]]; then
skip
fi

# Test docker local IP
run interface_ipv4 en0
[[ $output == "172.17.0.2" ]]
[[ $status == 0 ]]

# Test non-existing interface
run interface_ipv4 en69
[[ $output == "" ]]
[[ $status == 1 ]]
}
6 changes: 0 additions & 6 deletions test/stub.bats

This file was deleted.

0 comments on commit 2bc27a9

Please sign in to comment.