Skip to content

Commit ee66280

Browse files
committed
add new module: i3status
1 parent 305de9b commit ee66280

1 file changed

Lines changed: 248 additions & 0 deletions

File tree

py3status/modules/i3status.py

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
"""
2+
Run i3status modules and display its outputs in py3status.
3+
4+
Configuration parameters:
5+
format: display format for this module (default '{format_module}')
6+
format_module_separator: show separator if more than one (default ' ')
7+
general: specify settings for i3status general section
8+
(default {'colors': True, 'interval': 5})
9+
modules: specify a list of i3status modules and settings plus
10+
option format_module with {output} placeholder (default [])
11+
12+
Format placeholders:
13+
{format_module} format for i3status modules
14+
15+
Examples:
16+
```
17+
# add i3status modules
18+
# See `man i3status` for a full list of i3status configuration options.
19+
# Not all of i3status configuration options will be supported or usable.
20+
i3status {
21+
format_module_separator = "\?color=#666&show \| "
22+
general = {'colors': True, 'interval': 5}
23+
modules = [
24+
{
25+
"name": "ipv6",
26+
"format_down": "",
27+
"format_module": "\?if=output [\?color=darkgrey&show IPv6] {output}",
28+
},
29+
{
30+
"name": "wireless _first_",
31+
"format_up": "(%quality at %essid) %ip",
32+
"format_down": "",
33+
"format_module": "\?if=output [\?color=darkgrey&show Wireless] {output}",
34+
},
35+
{
36+
"name": "ethernet _first_",
37+
"format_up": "%ip (%speed)",
38+
"format_down": "",
39+
"format_module": "\?if=output [\?color=darkgrey&show Ethernet] {output}",
40+
},
41+
{
42+
"name": "battery all",
43+
"format_module": "\?if=output [\?color=darkgrey&show Battery] {output}",
44+
},
45+
{
46+
"name": "disk /",
47+
"format": "%avail",
48+
"format_module": "[\?color=darkgrey&show Disk] {output}",
49+
},
50+
{
51+
"name": "load",
52+
"format": "%1min",
53+
"format_module": "[\?color=darkgrey&show Load] {output}",
54+
},
55+
{
56+
"name": "memory",
57+
"format": "%percentage_used",
58+
"format_module": "[\?color=darkgrey&show Memory] {output}",
59+
},
60+
{
61+
"name": "tztime local",
62+
"format": "%Y-%m-%d %H:%M:%S",
63+
"format_module": "[\?color=darkgrey&show Time] {output}",
64+
},
65+
]
66+
}
67+
```
68+
69+
@author lasers
70+
71+
SAMPLE OUTPUT
72+
[
73+
{'full_text': 'W: ( 56% at lasers 5G)', 'color': '#00ff00'},
74+
{'full_text': ' | ', 'color': '#666'},
75+
{'full_text': 'E: down', 'color': '#ff0000'},
76+
]
77+
78+
disk_tztime
79+
]
80+
{'full_text': '1.2 TiB'}
81+
{'full_text': ' | ', 'color': '#666'},
82+
{'full_text': '2026-01-02 07:40:51 CST'}
83+
[
84+
"""
85+
86+
import json
87+
from contextlib import suppress
88+
from pathlib import Path
89+
from subprocess import PIPE, STDOUT, Popen
90+
from tempfile import NamedTemporaryFile
91+
from threading import Thread
92+
93+
MODULES = [
94+
{"name": "ipv6", "format_down": ""},
95+
{"name": "wireless _first_", "format_down": ""},
96+
{"name": "ethernet _first_", "format_down": ""},
97+
{"name": "battery all", "format_down": ""},
98+
{"name": "load", "format": "%1min"},
99+
{"name": "memory", "format": "%used"},
100+
{"name": "tztime local", "format": "%Y-%m-%d %H:%M:%S"},
101+
]
102+
SEPARATOR = r"\?color=#666&show \| "
103+
STRING_NOT_INSTALLED = "i3status not installed"
104+
105+
106+
class Py3status:
107+
""" """
108+
109+
# available configuration parameters
110+
format = "{format_module}"
111+
format_module_separator = " "
112+
general = {"colors": True, "interval": 5}
113+
modules = []
114+
115+
def post_config_hook(self):
116+
if not self.py3.check_commands("i3status"):
117+
raise Exception(STRING_NOT_INSTALLED)
118+
if not self.modules:
119+
(self.modules, self.format_module_separator) = (MODULES, SEPARATOR)
120+
for module in self.modules:
121+
if not isinstance(module, dict) or not module.get("name"):
122+
raise Exception("invalid modules")
123+
module.setdefault("format_module", "{output}")
124+
125+
self._write_i3status_config()
126+
self.i3status_command = ["i3status", "-c", self.tmpfile_name]
127+
self.error = None
128+
self.process = None
129+
self.running = True
130+
self.items = []
131+
self.line = ""
132+
133+
self.t = Thread(target=self._start_loop)
134+
self.t.daemon = True
135+
self.t.start()
136+
137+
def _write_i3status_config(self):
138+
def _format(value):
139+
if isinstance(value, bool):
140+
return f"{value}".lower()
141+
if isinstance(value, (int, float)):
142+
return f"{value}"
143+
return f'"{value}"'
144+
145+
# fmt: off
146+
try:
147+
# python 3.12+
148+
tmpfile = NamedTemporaryFile(mode="w", encoding="utf-8", prefix="py3status-i3status_",
149+
suffix=".conf", delete=False, delete_on_close=False)
150+
except TypeError:
151+
tmpfile = NamedTemporaryFile(mode="w", encoding="utf-8", prefix="py3status-i3status_",
152+
suffix=".conf", delete=False)
153+
self.tmpfile_name = tmpfile.name
154+
# fmt: on
155+
156+
lines = []
157+
general_settings = dict(self.general or {})
158+
general_settings.setdefault("output_format", "i3bar")
159+
lines.append("general {\n")
160+
for key, value in general_settings.items():
161+
lines.append(f" {key} = {_format(value)}\n")
162+
lines.append("}\n\n")
163+
for module in self.modules:
164+
lines.append(f'order += "{module["name"]}"\n')
165+
lines.append(f'{module["name"]} {{\n')
166+
for key, value in module.items():
167+
if key in ("name", "format_module"):
168+
continue
169+
lines.append(f" {key} = {_format(value)}\n")
170+
lines.append("}\n\n")
171+
tmpfile.write("".join(lines))
172+
tmpfile.flush()
173+
tmpfile.close()
174+
175+
def _parse_i3status_line(self, line):
176+
if not line or line in ["[", "]"] or line.startswith("{\"version\""):
177+
return None
178+
if line.startswith(","):
179+
line = line[1:]
180+
try:
181+
return json.loads(line)
182+
except ValueError:
183+
return None
184+
185+
def _format_modules(self):
186+
new_items = []
187+
for item, module in zip(self.items, self.modules):
188+
if not item.get("full_text"):
189+
continue
190+
item.pop("name", None)
191+
item.pop("instance", None)
192+
new_items.append(self.py3.safe_format(module["format_module"], {"output": item}))
193+
return new_items
194+
195+
def _cleanup(self):
196+
self.running = False
197+
if self.process and self.process.poll() is None:
198+
self.process.terminate()
199+
with suppress(FileNotFoundError):
200+
Path(self.tmpfile_name).unlink()
201+
self.py3.update()
202+
203+
def _start_loop(self):
204+
try:
205+
self.process = Popen(self.i3status_command, stdout=PIPE, stderr=STDOUT, text=True)
206+
while self.running:
207+
line = self.process.stdout.readline()
208+
if not line:
209+
if self.process.poll() is not None:
210+
raise Exception(self.line)
211+
continue
212+
line = line.strip()
213+
items = self._parse_i3status_line(line)
214+
if items is None:
215+
self.line = line
216+
continue
217+
if items == self.items:
218+
continue
219+
self.items = items
220+
self.py3.update()
221+
except Exception as err:
222+
self.error = " ".join(format(err).split()[1:])
223+
finally:
224+
self._cleanup()
225+
226+
def i3status(self):
227+
if self.error:
228+
self.py3.error(self.error, self.py3.CACHE_FOREVER)
229+
230+
format_module_separator = self.py3.safe_format(self.format_module_separator)
231+
format_module = self.py3.composite_join(format_module_separator, self._format_modules())
232+
233+
return {
234+
"cached_until": self.py3.CACHE_FOREVER,
235+
"full_text": self.py3.safe_format(self.format, {"format_module": format_module}),
236+
}
237+
238+
def kill(self):
239+
self._cleanup()
240+
241+
242+
if __name__ == "__main__":
243+
"""
244+
Run module in test mode.
245+
"""
246+
from py3status.module_test import module_test
247+
248+
module_test(Py3status)

0 commit comments

Comments
 (0)