Skip to content

Instantly share code, notes, and snippets.

@Kilobyte22
Created May 20, 2022 21:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Kilobyte22/7722dc81d534057be3809c7292e23f99 to your computer and use it in GitHub Desktop.
Save Kilobyte22/7722dc81d534057be3809c7292e23f99 to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby
require 'json'
require 'yaml'
require 'net/http'
KIBI = 1024
MEBI = KIBI * 1024
GIBI = MEBI * 1024
# kind can be (:wired, :wireless, :mobile, :local, :other)
NetworkInterface = Struct.new(:name, :kind, :address, :lldp, :link_state, :display_name, :bwinfo)
LLDPInfo = Struct.new(:device, :port, :vlan)
# Internet can be either of :connected (everything working), :disconnected (link down), :portal (captive portal)
NetworkData = Struct.new(:default_device, :internet, :data)
BWInfo = Struct.new(:out, :in)
def get_default_iface()
output = `ip -j r g 0.0.0.1`
if output.chomp.empty?
nil
else
JSON.parse(output).first['dev']
end
end
def get_online_state
begin
status = Net::HTTP.get_response(URI("http://detectportal.firefox.com/canonical.txt"))
case status.code
when "200" then :internet
when /^3..$/ then :portal
else
:unknown
end
rescue => _
:nointernet
end
end
def get_ifaces(state)
ifaces = JSON.parse(`ip -j a s`)
#routes = JSON.parse(`ip -j r s`)
lldp = JSON.parse(`lldpctl -f json`)["lldp"]["interface"] || {}
passed_time = Time.now.to_f - state[:timestamp]
data = ifaces.map do |iface|
ifname = iface['ifname']
bytes_tx = File.read("/sys/class/net/#{ifname}/statistics/tx_bytes").to_i
bytes_rx = File.read("/sys/class/net/#{ifname}/statistics/rx_bytes").to_i
bw_info = if state[:iface][ifname]
speed_tx = (bytes_tx - state[:iface][ifname][:last_tx]) / passed_time
speed_rx = (bytes_rx - state[:iface][ifname][:last_rx]) / passed_time
BWInfo.new(speed_tx, speed_rx)
end
kind = case ifname
when 'lo' then :local
when /^(eth\d+$|en[\d\w]+)$/ then :wired
when /^(wlan\d+|wl[\d\w]+)$/ then :wireless
when /^ww[\d\w]+$/ then :mobile # TODO: Determine signal strength
when /^tun[\d]+/ then :vpn
else
:other
end
display_name = case kind
when :local then "local network"
when :wireless then `iwgetid -r`.chomp
# TODO: Retrieve Carrier information for mobile networks
end
addrs = iface['addr_info'].map { |addr| "#{addr['local']}/#{addr['prefixlen']}" }
lldp_data = lldp.is_a?(Array) ? lldp.first : lldp
lldp_info = if lldp_data
iface_lldp = lldp_data[ifname]
lldp_info = if iface_lldp
dev_name = iface_lldp["chassis"].keys.first
port_name = iface_lldp["port"]["id"]["value"]
vlan_name = if iface_lldp["vlan"]
iface_lldp["vlan"]["vlan-id"]
end
LLDPInfo.new(dev_name, port_name, vlan_name)
end
end
state[:iface][ifname] = { last_tx: bytes_tx, last_rx: bytes_rx }
NetworkInterface.new(iface['ifname'], kind, addrs, lldp_info, iface['operstate'].downcase.to_sym, display_name, bw_info)
end
state[:timestamp] = Time.now.to_f
data
end
def filter_data(data, config, default = nil)
data.select do |iface|
config["iface_whitelist"].any?{|we| iface.name =~ Regexp.new(we)}||
(default && iface.name == default)||
!config["iface_blacklist"].all?{|be| iface.name =~ Regexp.new(be)}
end.sort_by { |iface| iface.link_state == :up ? 0 : 1 }
end
def network_name()
res = File.read("/etc/resolv.conf").lines
.map { |line| line.chomp.split(' ') }
.find { |line| line[0] == 'domain' }
return res[1] if res
res = File.read("/etc/resolv.conf").lines
.map { |line| line.chomp.split(' ') }
.find { |line| line[0] == 'search' }
res[1] if res
end
def render_speed(value)
value = value * 8
if value < KIBI
"%.2f" % value + "bit/s"
elsif value < MEBI
"%.2f" % (value / KIBI) + "kbit/s"
elsif value < GIBI
"%.2f" % (value / MEBI) + "Mbit/s"
else
"%.2f" % (value / GIBI) + "Gbit/s"
end
end
def display_net_info(iface)
color = iface.link_state == :down ? '#ff0000' : '#00ff00'
ret = ""
if iface.display_name
ret << "<span color=\"#{color}\">#{iface.display_name}</span> (#{iface.name})"
else
ret << "<span color=\"#{color}\">" << iface.name << "</span>"
end
if iface.link_state == :down
ret << ": DOWN"
else
if iface.bwinfo
ret << "\n #{render_speed(iface.bwinfo.out)}  #{render_speed(iface.bwinfo.in)}"
end
if iface.lldp
ret << "\nLLDP: [#{iface.lldp.device}] #{iface.lldp.port}"
ret << " | VLAN " << iface.lldp.vlan if iface.lldp.vlan
end
ret << iface.address.map { |addr| "\n <span color=\"#528df9\">" + addr + '</span>'}.join
end
end
def class_from_default(default, portal)
if default
case portal
when :portal then "portal"
when :internet then ["connected", default.kind.to_s]
when :unknown then "unknown"
when :nointernet then "unknown"
end
else
"disconnected"
end
end
def alt_from_default(default, portal)
if default
default.kind.to_s
else
"disconnected"
end
end
def display_from_default(default)
return 'Disconnected' if default.nil?
ret = default.display_name || "#{network_name()} (#{default.name})"
ret = default.name + ": " + default.address.first unless ret
ret = default.name unless ret
ret
end
def display_data(data, default, portal)
default_iface = data.find { |iface| iface.name == default }
net_name = display_from_default(default_iface)
{
text: net_name,
tooltip: data.map {|iface| display_net_info(iface) }.join("\n\n"),
class: class_from_default(default_iface, portal),
alt: alt_from_default(default_iface, portal)
}.to_json
end
config = YAML.load_file(File.expand_path("~/.config/waybar_network.yml"))
state = if File.exists?("/tmp/bwstate")
YAML.load_file("/tmp/bwstate")
else
{ timestamp: 0, iface: {} }
end
ifaces = get_ifaces(state)
default = get_default_iface()
ifaces = filter_data(ifaces, config, default)
online = get_online_state()
puts display_data(ifaces, default, online)
File.write("/tmp/bwstate", state.to_yaml)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment