Commit 20b2dfa

Eric Bower  ·  2026-09-02 22:27:13 -0400 EDT
parent 99bba65
refactor: use ergo status bar
5 files changed,  +241, -147
+16, -16
 1@@ -156,19 +156,19 @@ bindsym $mod+Shift+0 move container to workspace number 10
 2 
 3 # ==============================================================================
 4 
 5-bar {
 6-    position bottom
 7-    status_command python3 ~/.config/sway/status.py
 8-    font pango:$font
 9-
10-    colors {
11-        background $bg
12-        statusline $fg
13-        separator  $br_black
14-
15-        focused_workspace  $blue     $blue     $bg
16-        active_workspace   $br_black $br_black $fg
17-        inactive_workspace $bg       $bg       $white
18-        urgent_workspace   $red      $red      $bg
19-    }
20-}
21+# bar {
22+#     position bottom
23+#     status_command python3 ~/.config/sway/status.py
24+#     font pango:$font
25+# 
26+#     colors {
27+#         background $bg
28+#         statusline $fg
29+#         separator  $br_black
30+# 
31+#         focused_workspace  $blue     $blue     $bg
32+#         active_workspace   $br_black $br_black $fg
33+#         inactive_workspace $bg       $bg       $white
34+#         urgent_workspace   $red      $red      $bg
35+#     }
36+# }
+123, -0
  1@@ -0,0 +1,123 @@
  2+#!/usr/bin/env python3
  3+import json
  4+import os
  5+import subprocess
  6+import sys
  7+import time
  8+
  9+def get_sound():
 10+    try:
 11+        muted = subprocess.check_output(["pamixer", "--get-mute"], text=True, stderr=subprocess.DEVNULL).strip() == "true"
 12+        vol = subprocess.check_output(["pamixer", "--get-volume"], text=True, stderr=subprocess.DEVNULL).strip()
 13+        label = "MUTED" if muted else "VOL"
 14+        return f"{label} {vol}%"
 15+    except Exception:
 16+        return "VOL ?"
 17+
 18+def get_battery():
 19+    if not os.path.exists("/sys/class/power_supply"):
 20+        return None
 21+
 22+    try:
 23+        for bat in os.listdir("/sys/class/power_supply"):
 24+            if not bat.startswith("BAT"):
 25+                continue
 26+
 27+            path = f"/sys/class/power_supply/{bat}"
 28+            cap_file = f"{path}/capacity"
 29+            if not os.path.exists(cap_file):
 30+                continue
 31+
 32+            with open(cap_file) as f:
 33+                cap = f.read().strip()
 34+
 35+            status = "BAT"
 36+            stat_file = f"{path}/status"
 37+            if os.path.exists(stat_file):
 38+                with open(stat_file) as f:
 39+                    if f.read().strip() == "Charging":
 40+                        status = "CHR"
 41+
 42+            return f"{status} {cap}%"
 43+    except Exception:
 44+        pass
 45+
 46+    return None
 47+
 48+def get_brightness():
 49+    try:
 50+        act = subprocess.check_output(["brightnessctl", "-c", "backlight", "get"], text=True, stderr=subprocess.DEVNULL).strip()
 51+        max_b = subprocess.check_output(["brightnessctl", "-c", "backlight", "max"], text=True, stderr=subprocess.DEVNULL).strip()
 52+        return f"BRT {int(int(act) * 100 / int(max_b))}%"
 53+    except Exception:
 54+        return None
 55+
 56+def get_wifi_ssid(iface):
 57+    try:
 58+        out = subprocess.check_output(["iw", "dev", iface, "link"], text=True, stderr=subprocess.DEVNULL)
 59+        for line in out.splitlines():
 60+            if "SSID:" in line:
 61+                return line.split("SSID:", 1)[1].strip()
 62+    except Exception:
 63+        pass
 64+    return None
 65+
 66+def get_net():
 67+    try:
 68+        route = subprocess.check_output(["ip", "route", "show", "default"], text=True, stderr=subprocess.DEVNULL)
 69+    except Exception:
 70+        return "NET Disconnected"
 71+
 72+    if "dev" not in route:
 73+        return "NET Disconnected"
 74+
 75+    iface = route.split("dev")[1].split()[0]
 76+    if not iface.startswith(("wl", "wlan")):
 77+        return f"ETH {iface}"
 78+
 79+    ssid = get_wifi_ssid(iface)
 80+    return f"NET {ssid}" if ssid else f"NET {iface}"
 81+
 82+def get_time():
 83+    return time.strftime("%I:%M %m/%d").lstrip("0")
 84+
 85+def get_workspaces():
 86+    try:
 87+        out = subprocess.check_output(["swaymsg", "-t", "get_workspaces"], text=True, stderr=subprocess.DEVNULL)
 88+        workspaces = json.loads(out)
 89+    except Exception:
 90+        return None
 91+
 92+    if not workspaces:
 93+        return None
 94+
 95+    tags = [
 96+        f"^ {w['name']} ^" if w.get("focused") else f" {w['name']} "
 97+        for w in sorted(workspaces, key=lambda x: x.get("num", 0))
 98+    ]
 99+    return "".join(tags)
100+
101+def generate_line():
102+    status_parts = [p for p in [get_sound(), get_battery(), get_brightness(), get_net(), get_time()] if p]
103+    right_status = "  |  ".join(status_parts) + "  "
104+
105+    ws = get_workspaces()
106+    if not ws:
107+        return right_status
108+
109+    return f"{ws}    {right_status}"
110+
111+def main():
112+    try:
113+        while True:
114+            print(generate_line(), flush=True)
115+            time.sleep(1)
116+    except (BrokenPipeError, KeyboardInterrupt):
117+        try:
118+            sys.stdout.close()
119+        except Exception:
120+            pass
121+        sys.exit(0)
122+
123+if __name__ == "__main__":
124+    main()
+88, -131
  1@@ -1,151 +1,119 @@
  2 #!/usr/bin/env python3
  3 import json
  4-import sys
  5 import os
  6-import subprocess
  7 import shutil
  8+import subprocess
  9+import sys
 10 import threading
 11 import time
 12 
 13 def get_sound():
 14     try:
 15-        muted = subprocess.check_output(["pamixer", "--get-mute"], text=True).strip() == "true"
 16-        vol = subprocess.check_output(["pamixer", "--get-volume"], text=True).strip()
 17-        if muted:
 18-            return f"MUTED {vol}%"
 19-        return f"VOL {vol}%"
 20+        muted = subprocess.check_output(["pamixer", "--get-mute"], text=True, stderr=subprocess.DEVNULL).strip() == "true"
 21+        vol = subprocess.check_output(["pamixer", "--get-volume"], text=True, stderr=subprocess.DEVNULL).strip()
 22+        label = "MUTED" if muted else "VOL"
 23+        return f"{label} {vol}%"
 24     except Exception:
 25-        pass
 26-    return "VOL ?"
 27+        return "VOL ?"
 28 
 29 def get_battery():
 30+    if not os.path.exists("/sys/class/power_supply"):
 31+        return None
 32+
 33     try:
 34-        if not os.path.exists("/sys/class/power_supply"):
 35-            return None
 36-        bat_dirs = [d for d in os.listdir("/sys/class/power_supply") if d.startswith("BAT")]
 37-        if not bat_dirs:
 38-            return None
 39-        bat_path = os.path.join("/sys/class/power_supply", bat_dirs[0])
 40-        cap_file = os.path.join(bat_path, "capacity")
 41-        stat_file = os.path.join(bat_path, "status")
 42-        
 43-        if not os.path.exists(cap_file):
 44-            return None
 45-
 46-        with open(cap_file) as f:
 47-            cap = f.read().strip()
 48-        
 49-        status = "Discharging"
 50-        if os.path.exists(stat_file):
 51-            with open(stat_file) as f:
 52-                status = f.read().strip()
 53-
 54-        label = "CHR" if status == "Charging" else "BAT"
 55-        return f"{label} {cap}%"
 56+        for bat in os.listdir("/sys/class/power_supply"):
 57+            if not bat.startswith("BAT"):
 58+                continue
 59+
 60+            path = f"/sys/class/power_supply/{bat}"
 61+            cap_file = f"{path}/capacity"
 62+            if not os.path.exists(cap_file):
 63+                continue
 64+
 65+            with open(cap_file) as f:
 66+                cap = f.read().strip()
 67+
 68+            status = "BAT"
 69+            stat_file = f"{path}/status"
 70+            if os.path.exists(stat_file):
 71+                with open(stat_file) as f:
 72+                    if f.read().strip() == "Charging":
 73+                        status = "CHR"
 74+
 75+            return f"{status} {cap}%"
 76     except Exception:
 77-        return None
 78+        pass
 79+
 80+    return None
 81 
 82 def get_brightness():
 83     try:
 84-        if not os.path.exists("/sys/class/backlight") or not os.listdir("/sys/class/backlight"):
 85-            return None
 86-        actual = int(subprocess.check_output(["brightnessctl", "-c", "backlight", "get"], text=True).strip())
 87-        max_b = int(subprocess.check_output(["brightnessctl", "-c", "backlight", "max"], text=True).strip())
 88-        pct = int((actual / max_b) * 100)
 89-        return f"BRT {pct}%"
 90+        act = subprocess.check_output(["brightnessctl", "-c", "backlight", "get"], text=True, stderr=subprocess.DEVNULL).strip()
 91+        max_b = subprocess.check_output(["brightnessctl", "-c", "backlight", "max"], text=True, stderr=subprocess.DEVNULL).strip()
 92+        return f"BRT {int(int(act) * 100 / int(max_b))}%"
 93     except Exception:
 94         return None
 95 
 96-def get_net():
 97-    if shutil.which("nmcli"):
 98-        try:
 99-            out = subprocess.check_output(
100-                ["nmcli", "-t", "-f", "TYPE,STATE,CONNECTION", "dev"], 
101-                text=True
102-            ).strip()
103-            
104-            eth_name = None
105-            wifi_ssid = None
106-            
107-            for line in out.splitlines():
108-                parts = line.split(":")
109-                if len(parts) >= 3 and parts[1] == "connected":
110-                    dev_type = parts[0]
111-                    conn_name = parts[2]
112-                    if dev_type == "ethernet":
113-                        eth_name = conn_name if conn_name else "Wired"
114-                    elif dev_type == "wifi":
115-                        wifi_ssid = conn_name if conn_name else "WiFi"
116-                        
117-            if eth_name:
118-                return f"ETH {eth_name}"
119-            elif wifi_ssid:
120-                return f"NET {wifi_ssid}"
121-        except Exception:
122-            pass
123-
124+def get_wifi_ssid(iface):
125     try:
126-        out = subprocess.check_output(["ip", "route", "show", "default"], text=True).strip()
127-        if out:
128-            tokens = out.split()
129-            if "dev" in tokens:
130-                iface = tokens[tokens.index("dev") + 1]
131-                is_wifi = (
132-                    os.path.exists(f"/sys/class/net/{iface}/wireless")
133-                    or os.path.exists(f"/sys/class/net/{iface}/phy80211")
134-                    or iface.startswith(("wl", "wlan"))
135-                )
136-                if is_wifi:
137-                    ssid = None
138-                    if shutil.which("iw"):
139-                        try:
140-                            iw_out = subprocess.check_output(["iw", "dev", iface, "link"], text=True)
141-                            for line in iw_out.splitlines():
142-                                if "SSID:" in line:
143-                                    ssid = line.split("SSID:", 1)[1].strip()
144-                                    break
145-                        except Exception:
146-                            pass
147-                    elif shutil.which("iwgetid"):
148-                        try:
149-                            ssid = subprocess.check_output(["iwgetid", "-r"], text=True).strip()
150-                        except Exception:
151-                            pass
152-                    return f"NET {ssid}" if ssid else f"NET {iface}"
153-                else:
154-                    return f"ETH {iface}"
155+        out = subprocess.check_output(["iw", "dev", iface, "link"], text=True, stderr=subprocess.DEVNULL)
156+        for line in out.splitlines():
157+            if "SSID:" in line:
158+                return line.split("SSID:", 1)[1].strip()
159     except Exception:
160         pass
161+    return None
162+
163+def get_net():
164+    try:
165+        route = subprocess.check_output(["ip", "route", "show", "default"], text=True, stderr=subprocess.DEVNULL)
166+    except Exception:
167+        return "NET Disconnected"
168 
169-    return "NET Disconnected"
170+    if "dev" not in route:
171+        return "NET Disconnected"
172+
173+    iface = route.split("dev")[1].split()[0]
174+    if not iface.startswith(("wl", "wlan")):
175+        return f"ETH {iface}"
176+
177+    ssid = get_wifi_ssid(iface)
178+    return f"NET {ssid}" if ssid else f"NET {iface}"
179 
180 def get_time():
181     return time.strftime("%I:%M %m/%d").lstrip("0")
182 
183 def generate_blocks():
184-    blocks = []
185-    
186-    # Sound (Monokai Green #a6e22e)
187-    blocks.append({"name": "sound", "full_text": get_sound(), "color": "#a6e22e"})
188-    
189-    # Battery (Monokai Yellow #e6db74)
190+    blocks = [
191+        {"name": "sound", "full_text": get_sound(), "color": "#a6e22e"},
192+    ]
193+
194     bat = get_battery()
195     if bat:
196         blocks.append({"name": "battery", "full_text": bat, "color": "#e6db74"})
197-        
198-    # Brightness (Monokai Cyan #66d9ef)
199+
200     bright = get_brightness()
201     if bright:
202         blocks.append({"name": "brightness", "full_text": bright, "color": "#66d9ef"})
203-        
204-    # Network (Ethernet / WiFi) (Monokai Purple #ae81ff)
205-    blocks.append({"name": "wifi", "full_text": get_net(), "color": "#ae81ff"})
206-    
207-    # Clock (Monokai White #f8f8f2)
208-    blocks.append({"name": "clock", "full_text": get_time(), "color": "#f8f8f2"})
209-    
210+
211+    blocks.extend([
212+        {"name": "wifi", "full_text": get_net(), "color": "#ae81ff"},
213+        {"name": "clock", "full_text": get_time(), "color": "#f8f8f2"},
214+    ])
215+
216     return blocks
217 
218+def handle_click(block_name, button):
219+    if button != 1:
220+        return
221+
222+    if block_name == "wifi":
223+        cmd = ["monstar", "-e", "nmtui"] if shutil.which("monstar") else ["foot", "-e", "nmtui"]
224+        subprocess.Popen(cmd)
225+    elif block_name == "sound":
226+        cmd = ["pavucontrol"] if shutil.which("pavucontrol") else ["pamixer", "-t"]
227+        subprocess.Popen(cmd)
228+
229 def listen_clicks():
230     while True:
231         try:
232@@ -155,40 +123,29 @@ def listen_clicks():
233             clean_line = line.strip().lstrip("[,").rstrip(",")
234             if not clean_line:
235                 continue
236+
237             event = json.loads(clean_line)
238-            block_name = event.get("name")
239-            button = event.get("button", 1)
240-            
241-            if button == 1:
242-                if block_name == "wifi":
243-                    try:
244-                        subprocess.Popen(["monstar", "-e", "nmtui"])
245-                    except FileNotFoundError:
246-                        subprocess.Popen(["foot", "-e", "nmtui"])
247-                elif block_name == "sound":
248-                    try:
249-                        subprocess.Popen(["pavucontrol"])
250-                    except FileNotFoundError:
251-                        subprocess.Popen(["pamixer", "-t"])
252+            handle_click(event.get("name"), event.get("button", 1))
253         except Exception:
254             pass
255 
256 def main():
257     try:
258-        # i3bar / swaybar JSON protocol header
259         print(json.dumps({"version": 1, "click_events": True}))
260         print("[")
261         sys.stdout.flush()
262-        
263-        # Thread to handle click events on stdin
264+
265         threading.Thread(target=listen_clicks, daemon=True).start()
266-        
267+
268         while True:
269-            blocks = generate_blocks()
270-            print(json.dumps(blocks) + ",")
271+            print(json.dumps(generate_blocks()) + ",")
272             sys.stdout.flush()
273             time.sleep(1)
274     except (BrokenPipeError, KeyboardInterrupt):
275+        try:
276+            sys.stdout.close()
277+        except Exception:
278+            pass
279         sys.exit(0)
280 
281 if __name__ == "__main__":
+13, -0
 1@@ -0,0 +1,13 @@
 2+[Unit]
 3+Description=Ergo Wayland status bar
 4+PartOf=graphical-session.target
 5+After=graphical-session.target
 6+Requisite=graphical-session.target
 7+
 8+[Service]
 9+Type=simple
10+ExecStart=/bin/sh -c 'python3 %h/.config/sway/ergo_status.py | %h/.local/bin/ergo -b -r -f "JetBrainsMono Nerd Font 10.5" -N 272822 -n f8f8f2 -S 66d9ef -s 272822'
11+Restart=on-failure
12+
13+[Install]
14+WantedBy=wayland-session.target
+1, -0
1@@ -20,6 +20,7 @@ WAYLAND_UNITS=(
2   swayidle.service
3   swaylock.service
4   wlsunset.service
5+  ergo.service
6 )
7 
8 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"