Created
May 20, 2022 21:53
-
-
Save Kilobyte22/7722dc81d534057be3809c7292e23f99 to your computer and use it in GitHub Desktop.
This file contains 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 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