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)"