Last active
January 28, 2026 09:44
-
-
Save Kilobyte22/cbfb58bd4d8c2699bdecf64fce7a2eb7 to your computer and use it in GitHub Desktop.
CSS Classes: connected (in combination with wireless, wired, mobile, other or local), disconnected, or portal (in combination with same as connected)
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
| { | |
| "custom/network": { | |
| "exec": "~/.scripts/waybar/network", | |
| "return-type": "json", | |
| "interval": 5, | |
| "format": " {icon} {}", | |
| "tooltip": true, | |
| "format-icons": { | |
| "disconnected": "", | |
| "wired": "", | |
| "wireless": "", | |
| "mobile": "" | |
| } | |
| } | |
| } |
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 ruby | |
| require 'json' | |
| require 'yaml' | |
| 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_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 | |
| 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.first | |
| 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) | |
| data.select do |iface| | |
| config["iface_whitelist"].any?{|we| iface.name =~ Regexp.new(we)}|| | |
| !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 | |
| ret << "\n #{render_speed(iface.bwinfo.out)} #{render_speed(iface.bwinfo.in)}" | |
| 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=\"#0000ff\">" + addr + '</span>'}.join | |
| end | |
| end | |
| def class_from_default(default, portal) | |
| if default | |
| if portal | |
| "portal" | |
| else | |
| ["connected", default.kind.to_s] | |
| 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() | |
| ret = default.name + ": " + default.address.first unless ret | |
| ret = default.name unless ret | |
| ret | |
| end | |
| def display_data(data, default) | |
| default_iface = data.find { |iface| iface.name == default } | |
| portal = nil | |
| 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) | |
| puts display_data(ifaces, default) | |
| File.write("/tmp/bwstate", state.to_yaml) |
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
| iface_whitelist: | |
| - "^en[\\d\\w]+$" | |
| - "^eth\\d+$" | |
| - "^wl[\\d\\w]+$" | |
| - "^wlan\\d+$" | |
| - "^ww[\\d\\w]+$" | |
| iface_blacklist: [] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment