Manual fixes to Python file
To see if I like the rules
This commit is contained in:
parent
34b545890d
commit
8179433c41
10 changed files with 612 additions and 547 deletions
|
@ -1,3 +1,8 @@
|
|||
#!/usr/bin/env nix-shell
|
||||
#! nix-shell -i python3 --pure
|
||||
#! nix-shell -p python3 python3Packages.colorspacious python3Packages.numpy
|
||||
# vim: filetype=python
|
||||
|
||||
import argparse
|
||||
import json
|
||||
|
||||
|
@ -90,14 +95,14 @@ if args.polarity == "light":
|
|||
frogarized_rgb = np.vstack([frogarized_rgb[7::-1], frogarized_rgb[8:]])
|
||||
|
||||
# Output
|
||||
palette = dict()
|
||||
palette = {}
|
||||
for i in range(16):
|
||||
rgb = frogarized_rgb[i]
|
||||
r, g, b = rgb
|
||||
hex = f"#{r:02x}{g:02x}{b:02x}"
|
||||
palette[f"base{i:02X}"] = hex
|
||||
hexa = f"#{r:02x}{g:02x}{b:02x}"
|
||||
palette[f"base{i:02X}"] = hexa
|
||||
if args.output == "truecolor":
|
||||
print(f"\033[48;2;{r};{g};{b}m{hex}\033[0m") # ]]
|
||||
print(f"\033[48;2;{r};{g};{b}m{hexa}\033[0m") # ]]
|
||||
# treesitter is silly and will consider brackets in strings
|
||||
# as indentation, hence the comment above
|
||||
if args.output == "json":
|
||||
|
|
28
common/update-local-flakes/update-local-flakes.py
Executable file → Normal file
28
common/update-local-flakes/update-local-flakes.py
Executable file → Normal file
|
@ -1,6 +1,6 @@
|
|||
import argparse
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import subprocess
|
||||
|
||||
GET_INPUTS_CMD = [
|
||||
|
@ -12,28 +12,26 @@ GET_INPUTS_CMD = [
|
|||
]
|
||||
|
||||
|
||||
def process_flake(flakeUri: str) -> None:
|
||||
def process_flake(flake_uri: pathlib.Path) -> None:
|
||||
# get full path
|
||||
flakeUri = os.path.normpath(flakeUri)
|
||||
flakeFile = os.path.join(flakeUri, "flake.nix")
|
||||
if not os.path.isfile(flakeFile):
|
||||
raise FileNotFoundError(f"Flake not found: {flakeUri}")
|
||||
flake_file = flake_uri / "flake.nix"
|
||||
if not flake_file.is_file():
|
||||
msg = f"Flake not found: {flake_uri}"
|
||||
raise FileNotFoundError(msg)
|
||||
# import dependencies
|
||||
p = subprocess.run(GET_INPUTS_CMD, cwd=flakeUri, stdout=subprocess.PIPE, check=True)
|
||||
p = subprocess.run(
|
||||
GET_INPUTS_CMD, cwd=flake_uri, stdout=subprocess.PIPE, check=True
|
||||
)
|
||||
deps = json.loads(p.stdout)
|
||||
# for each dependency
|
||||
for dep_name, dep in deps.items():
|
||||
dep_url = dep["url"]
|
||||
# if not local path, continue
|
||||
if not (
|
||||
dep_url.startswith("path:") or dep_url.startswith("git+file:")
|
||||
):
|
||||
if not dep_url.startswith(("path:", "git+file:")):
|
||||
continue
|
||||
if dep.get("flake", True):
|
||||
# get flake file corresponding
|
||||
dep_path = dep_url.split(":")[1]
|
||||
if not dep_path.startswith("/"):
|
||||
dep_path = os.path.join(flakeUri, dep_path)
|
||||
dep_path = pathlib.Path(flake_uri, dep_url.split(":")[1])
|
||||
process_flake(dep_path)
|
||||
# update lockfile
|
||||
cmd = [
|
||||
|
@ -46,7 +44,7 @@ def process_flake(flakeUri: str) -> None:
|
|||
"update",
|
||||
dep_name,
|
||||
]
|
||||
p = subprocess.run(cmd, cwd=flakeUri, check=True)
|
||||
p = subprocess.run(cmd, cwd=flake_uri, check=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
@ -54,6 +52,6 @@ if __name__ == "__main__":
|
|||
description="Recursively update lockfiles "
|
||||
"of flakes located on the system"
|
||||
)
|
||||
parser.add_argument("flake", help="Starting flake", default="/")
|
||||
parser.add_argument("flake", help="Starting flake", type=pathlib.Path)
|
||||
args = parser.parse_args()
|
||||
process_flake(args.flake)
|
||||
|
|
272
curacao/desk/desk_mqtt.py
Executable file → Normal file
272
curacao/desk/desk_mqtt.py
Executable file → Normal file
|
@ -12,7 +12,9 @@ import usb.util
|
|||
|
||||
class Desk:
|
||||
"""
|
||||
Controls my Linak desk, which is a CBD4P controller connected via USB2LIN06
|
||||
Controls my Linak desk.
|
||||
|
||||
It is a CBD4P controller connected via USB2LIN06.
|
||||
This particular combination doesn't seem to report desk height,
|
||||
so it is estimated from the physical controller that does work.
|
||||
"""
|
||||
|
@ -69,24 +71,27 @@ class Desk:
|
|||
# Better estimate a bit slower
|
||||
SPEED = (VALUE_TOP - VALUE_BOT) / FULL_TIME * SPEED_MARGIN # unit / s
|
||||
|
||||
def _cmToUnit(self, height: float) -> int:
|
||||
HEADER_STRUCT = struct.Struct("<BB")
|
||||
REPORT_STRUCT = struct.Struct("<HH14xH36x")
|
||||
|
||||
def _cm_to_unit(self, height: float) -> int:
|
||||
return round((height - self.HEIGHT_OFFSET) * self.HEIGHT_MULT)
|
||||
|
||||
def _unitToCm(self, height: int) -> float:
|
||||
def _unit_to_cm(self, height: int) -> float:
|
||||
return height / self.HEIGHT_MULT + self.HEIGHT_OFFSET
|
||||
|
||||
def _get(self, typ: int, overflow_ok: bool = False) -> bytes:
|
||||
def _get(self, typ: int) -> bytes:
|
||||
# Magic numbers: get class interface, HID get report
|
||||
raw = self._dev.ctrl_transfer(
|
||||
0xA1, 0x01, 0x300 + typ, 0, self.BUF_LEN
|
||||
).tobytes()
|
||||
self.log.debug(f"Received {raw.hex()}")
|
||||
assert raw[0] == typ
|
||||
size = raw[1]
|
||||
end = 2 + size
|
||||
if not overflow_ok:
|
||||
assert end < self.BUF_LEN
|
||||
return raw[2:end]
|
||||
self.log.debug("Received %s", raw.hex())
|
||||
typ, size = self.HEADER_STRUCT.unpack(raw[: self.HEADER_STRUCT.size])
|
||||
start = self.HEADER_STRUCT.size
|
||||
end = self.HEADER_STRUCT.size + size
|
||||
if end >= self.BUF_LEN:
|
||||
raise OverflowError
|
||||
return raw[start:end]
|
||||
# Non-implemented types:
|
||||
# 1, 7: some kind of stream when the device isn't initialized?
|
||||
# size reduces the faster you poll, increases when buttons are held
|
||||
|
@ -96,7 +101,7 @@ class Desk:
|
|||
buf = bytes([typ]) + buf
|
||||
# The official apps pad, not that it doesn't seem to work without
|
||||
buf = buf + b"\x00" * (self.BUF_LEN - len(buf))
|
||||
self.log.debug(f"Sending {buf.hex()}")
|
||||
self.log.debug("Sending %s", buf.hex())
|
||||
# Magic numbers: set class interface, HID set report
|
||||
self._dev.ctrl_transfer(0x21, 0x09, 0x300 + typ, 0, buf)
|
||||
# Non-implemented types:
|
||||
|
@ -110,9 +115,11 @@ class Desk:
|
|||
|
||||
def _initialize(self) -> None:
|
||||
"""
|
||||
Seems to take the USB2LIN06 out of "boot mode"
|
||||
(name according to CBD4 Controller) which it is after reset.
|
||||
Seems to take the USB2LIN06 out of "boot mode".
|
||||
|
||||
It is like that after reset.
|
||||
Permits control and reading the report.
|
||||
(name according to CBD4 Controller)
|
||||
"""
|
||||
buf = bytes([0x04, 0x00, 0xFB])
|
||||
self._set(3, buf)
|
||||
|
@ -122,9 +129,8 @@ class Desk:
|
|||
self.log = logging.getLogger("Desk")
|
||||
self._dev = usb.core.find(idVendor=Desk.VEND, idProduct=Desk.PROD)
|
||||
if not self._dev:
|
||||
raise ValueError(
|
||||
f"Device {Desk.VEND}:" f"{Desk.PROD:04d} " f"not found!"
|
||||
)
|
||||
msg = f"Device {Desk.VEND}: {Desk.PROD:04d} not found!"
|
||||
raise ValueError(msg)
|
||||
|
||||
if self._dev.is_kernel_driver_active(0):
|
||||
self._dev.detach_kernel_driver(0)
|
||||
|
@ -136,9 +142,7 @@ class Desk:
|
|||
self.fetch_callback: typing.Callable[[Desk], None] | None = None
|
||||
|
||||
def _get_report(self) -> bytes:
|
||||
raw = self._get(4)
|
||||
assert len(raw) == 0x38
|
||||
return raw
|
||||
return self._get(4)
|
||||
|
||||
def _update_estimations(self) -> None:
|
||||
now = time.time()
|
||||
|
@ -192,23 +196,19 @@ class Desk:
|
|||
try:
|
||||
raw = self._get_report()
|
||||
break
|
||||
except usb.USBError as e:
|
||||
self.log.error(e)
|
||||
except usb.USBError:
|
||||
self.log.exception("USB issue")
|
||||
else:
|
||||
raw = self._get_report()
|
||||
|
||||
# Allegedly, from decompiling:
|
||||
# https://www.linak-us.com/products/controls/desk-control-basic-software/
|
||||
# Never reports anything in practice
|
||||
self.value = struct.unpack("<H", raw[0:2])[0]
|
||||
unk = struct.unpack("<H", raw[2:4])[0]
|
||||
# Most values are from examining the basic software.
|
||||
# Never reports anything in practice.
|
||||
# Destination is from observation.
|
||||
self.value, unk, self.destination = self.REPORT_STRUCT.unpack(raw)
|
||||
self.initalized = (unk & 0xF) != 0
|
||||
|
||||
# From observation. Reliable
|
||||
self.destination = (struct.unpack("<H", raw[18:20])[0],)[0]
|
||||
|
||||
if self.destination != self.last_destination:
|
||||
self.log.info(f"Destination changed to {self.destination:04x}")
|
||||
self.log.info("Destination changed to %04x", self.destination)
|
||||
self.last_destination = self.destination
|
||||
|
||||
self._update_estimations()
|
||||
|
@ -224,7 +224,7 @@ class Desk:
|
|||
position = max(self.VALUE_BOT, position)
|
||||
position = min(self.VALUE_TOP, position)
|
||||
|
||||
self.log.info(f"Start moving to {position:04x}")
|
||||
self.log.info("Start moving to %04x", position)
|
||||
self.fetch()
|
||||
while self.est_value != position:
|
||||
self._move(position)
|
||||
|
@ -234,6 +234,8 @@ class Desk:
|
|||
|
||||
def move_to(self, position: float) -> None:
|
||||
"""
|
||||
Move the desk to the given position in cm.
|
||||
|
||||
If any button is held during movement, the desk will stop moving,
|
||||
yet this will think it's still moving, throwing off the estimates.
|
||||
It's not a bug, it's a safety feature.
|
||||
|
@ -243,7 +245,7 @@ class Desk:
|
|||
"""
|
||||
# Would to stop for a while before reversing course, without being able
|
||||
# to read the actual height it's just too annoying to implement
|
||||
return self._move_to(self._cmToUnit(position))
|
||||
return self._move_to(self._cm_to_unit(position))
|
||||
|
||||
def stop(self) -> None:
|
||||
self.log.info("Stop moving")
|
||||
|
@ -252,128 +254,144 @@ class Desk:
|
|||
|
||||
def get_height_bounds(self) -> tuple[float, float]:
|
||||
return (
|
||||
self._unitToCm(int(self.est_value_bot)),
|
||||
self._unitToCm(int(self.est_value_top)),
|
||||
self._unit_to_cm(int(self.est_value_bot)),
|
||||
self._unit_to_cm(int(self.est_value_top)),
|
||||
)
|
||||
|
||||
def get_height(self) -> float | None:
|
||||
if self.est_value is None:
|
||||
return None
|
||||
return self._unitToCm(self.est_value)
|
||||
return self._unit_to_cm(self.est_value)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig()
|
||||
log = logging.getLogger(__name__)
|
||||
class App:
|
||||
NDIGITS = 1
|
||||
|
||||
desk = Desk()
|
||||
serial = "000C-34E7"
|
||||
def __init__(self) -> None:
|
||||
logging.basicConfig()
|
||||
self.log = logging.getLogger(__name__)
|
||||
|
||||
# Configure the required parameters for the MQTT broker
|
||||
mqtt_settings = ha_mqtt_discoverable.Settings.MQTT(host="192.168.7.53")
|
||||
ndigits = 1
|
||||
target_height: float | None = None
|
||||
self.desk = Desk()
|
||||
self.desk.fetch_callback = self.fetch_callback
|
||||
serial = "000C-34E7"
|
||||
|
||||
device_info = ha_mqtt_discoverable.DeviceInfo(
|
||||
name="Desk",
|
||||
identifiers=["Linak", serial],
|
||||
manufacturer="Linak",
|
||||
model="CBD4P",
|
||||
suggested_area="Desk",
|
||||
hw_version="77402",
|
||||
sw_version="1.91",
|
||||
serial_number=serial,
|
||||
)
|
||||
# Configure the required parameters for the MQTT broker
|
||||
mqtt_settings = ha_mqtt_discoverable.Settings.MQTT(host="192.168.7.53")
|
||||
self.target_height: float | None = None
|
||||
|
||||
common_opts = {
|
||||
"device": device_info,
|
||||
"icon": "mdi:desk",
|
||||
"unit_of_measurement": "cm",
|
||||
"device_class": "distance",
|
||||
"expire_after": 10,
|
||||
}
|
||||
# TODO Implement proper availability in hq-mqtt-discoverable
|
||||
device_info = ha_mqtt_discoverable.DeviceInfo(
|
||||
name="Desk",
|
||||
identifiers=["Linak", serial],
|
||||
manufacturer="Linak",
|
||||
model="CBD4P",
|
||||
suggested_area="Desk",
|
||||
hw_version="77402",
|
||||
sw_version="1.91",
|
||||
serial_number=serial,
|
||||
)
|
||||
|
||||
height_info = ha_mqtt_discoverable.sensors.NumberInfo(
|
||||
name="Height ",
|
||||
min=desk.HEIGHT_BOT,
|
||||
max=desk.HEIGHT_TOP,
|
||||
mode="slider",
|
||||
step=10 ** (-ndigits),
|
||||
unique_id="desk_height",
|
||||
**common_opts,
|
||||
)
|
||||
height_settings = ha_mqtt_discoverable.Settings(
|
||||
mqtt=mqtt_settings, entity=height_info
|
||||
)
|
||||
common_opts = {
|
||||
"device": device_info,
|
||||
"icon": "mdi:desk",
|
||||
"unit_of_measurement": "cm",
|
||||
"device_class": "distance",
|
||||
"expire_after": 10,
|
||||
}
|
||||
# TODO Implement proper availability in hq-mqtt-discoverable
|
||||
|
||||
def height_callback(
|
||||
client: paho.mqtt.client.Client,
|
||||
user_data: None,
|
||||
message: paho.mqtt.client.MQTTMessage,
|
||||
) -> None:
|
||||
global target_height
|
||||
target_height = float(message.payload.decode())
|
||||
log.info(f"Requested height to {target_height:.1f}")
|
||||
height_info = ha_mqtt_discoverable.sensors.NumberInfo(
|
||||
name="Height ",
|
||||
min=self.desk.HEIGHT_BOT,
|
||||
max=self.desk.HEIGHT_TOP,
|
||||
mode="slider",
|
||||
step=10 ** (-self.NDIGITS),
|
||||
unique_id="desk_height",
|
||||
**common_opts,
|
||||
)
|
||||
height_settings = ha_mqtt_discoverable.Settings(
|
||||
mqtt=mqtt_settings, entity=height_info
|
||||
)
|
||||
|
||||
height = ha_mqtt_discoverable.sensors.Number(
|
||||
height_settings, height_callback
|
||||
)
|
||||
self.height = ha_mqtt_discoverable.sensors.Number(
|
||||
height_settings, self.height_callback
|
||||
)
|
||||
|
||||
height_max_info = ha_mqtt_discoverable.sensors.SensorInfo(
|
||||
name="Estimated height max",
|
||||
unique_id="desk_height_max",
|
||||
entity_category="diagnostic",
|
||||
**common_opts,
|
||||
)
|
||||
height_max_settings = ha_mqtt_discoverable.Settings(
|
||||
mqtt=mqtt_settings, entity=height_max_info
|
||||
)
|
||||
height_max = ha_mqtt_discoverable.sensors.Sensor(height_max_settings)
|
||||
height_max_info = ha_mqtt_discoverable.sensors.SensorInfo(
|
||||
name="Estimated height max",
|
||||
unique_id="desk_height_max",
|
||||
entity_category="diagnostic",
|
||||
**common_opts,
|
||||
)
|
||||
height_max_settings = ha_mqtt_discoverable.Settings(
|
||||
mqtt=mqtt_settings, entity=height_max_info
|
||||
)
|
||||
self.height_max = ha_mqtt_discoverable.sensors.Sensor(
|
||||
height_max_settings
|
||||
)
|
||||
|
||||
height_min_info = ha_mqtt_discoverable.sensors.SensorInfo(
|
||||
name="Estimated height min",
|
||||
unique_id="desk_height_min",
|
||||
entity_category="diagnostic",
|
||||
**common_opts,
|
||||
)
|
||||
height_min_settings = ha_mqtt_discoverable.Settings(
|
||||
mqtt=mqtt_settings, entity=height_min_info
|
||||
)
|
||||
height_min = ha_mqtt_discoverable.sensors.Sensor(height_min_settings)
|
||||
height_min_info = ha_mqtt_discoverable.sensors.SensorInfo(
|
||||
name="Estimated height min",
|
||||
unique_id="desk_height_min",
|
||||
entity_category="diagnostic",
|
||||
**common_opts,
|
||||
)
|
||||
height_min_settings = ha_mqtt_discoverable.Settings(
|
||||
mqtt=mqtt_settings, entity=height_min_info
|
||||
)
|
||||
self.height_min = ha_mqtt_discoverable.sensors.Sensor(
|
||||
height_min_settings
|
||||
)
|
||||
|
||||
last_published_state = None
|
||||
self.last_published_state: tuple[float | None, float, float] | None = (
|
||||
None
|
||||
)
|
||||
|
||||
def fetch_callback(desk: Desk) -> None:
|
||||
def fetch_callback(self, desk: Desk) -> None:
|
||||
hcur = desk.get_height()
|
||||
hmin, hmax = desk.get_height_bounds()
|
||||
global last_published_state
|
||||
|
||||
state = hcur, hmin, hmax
|
||||
if state == last_published_state:
|
||||
if state == self.last_published_state:
|
||||
return
|
||||
last_published_state = state
|
||||
self.last_published_state = state
|
||||
|
||||
# If none this will set as unknown
|
||||
# Also readings can be a bit outside the boundaries,
|
||||
# so this skips verification
|
||||
if isinstance(hcur, float):
|
||||
hcur = round(hcur, ndigits=ndigits)
|
||||
height._update_state(hcur)
|
||||
hcur = round(hcur, ndigits=self.NDIGITS)
|
||||
self.height._update_state(hcur) # noqa: SLF001
|
||||
|
||||
height_max._update_state(round(hmax, ndigits=ndigits))
|
||||
height_min._update_state(round(hmin, ndigits=ndigits))
|
||||
self.height_max._update_state( # noqa: SLF001
|
||||
round(hmax, ndigits=self.NDIGITS),
|
||||
)
|
||||
self.height_min._update_state( # noqa: SLF001
|
||||
round(hmin, ndigits=self.NDIGITS),
|
||||
)
|
||||
|
||||
desk.fetch_callback = fetch_callback
|
||||
def height_callback(
|
||||
self,
|
||||
_client: paho.mqtt.client.Client,
|
||||
_user_data: None,
|
||||
message: paho.mqtt.client.MQTTMessage,
|
||||
) -> None:
|
||||
self.target_height = float(message.payload.decode())
|
||||
self.log.info("Requested height to %.1f", self.target_height)
|
||||
|
||||
interval = 0.2
|
||||
# Need to be rective to catch
|
||||
while True:
|
||||
if target_height:
|
||||
temp_target_height = target_height
|
||||
# Allows queuing of other instructions while moving
|
||||
target_height = None
|
||||
desk.move_to(temp_target_height)
|
||||
else:
|
||||
time.sleep(interval)
|
||||
desk.fetch()
|
||||
def run(self) -> None:
|
||||
interval = 0.2
|
||||
# Need to be rective to catch
|
||||
while True:
|
||||
if self.target_height:
|
||||
temp_target_height = self.target_height
|
||||
# Allows queuing of other instructions while moving
|
||||
self.target_height = None
|
||||
self.desk.move_to(temp_target_height)
|
||||
else:
|
||||
time.sleep(interval)
|
||||
self.desk.fetch()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = App()
|
||||
app.run()
|
||||
|
|
|
@ -9,7 +9,7 @@ from frobar.common import Alignment
|
|||
|
||||
def main() -> None:
|
||||
# TODO Configurable
|
||||
FROGARIZED = [
|
||||
frogarized = (
|
||||
"#092c0e",
|
||||
"#143718",
|
||||
"#5a7058",
|
||||
|
@ -26,12 +26,12 @@ def main() -> None:
|
|||
"#008dd1",
|
||||
"#5c73c4",
|
||||
"#d43982",
|
||||
]
|
||||
)
|
||||
# TODO Not super happy with the color management,
|
||||
# while using an existing library is great, it's limited to ANSI colors
|
||||
|
||||
def base16_color(color: int) -> tuple[int, int, int]:
|
||||
hexa = FROGARIZED[color]
|
||||
hexa = frogarized[color]
|
||||
return tuple(rich.color.parse_rgb_hex(hexa[1:]))
|
||||
|
||||
theme = rich.terminal_theme.TerminalTheme(
|
||||
|
@ -62,83 +62,83 @@ def main() -> None:
|
|||
)
|
||||
|
||||
bar = frobar.common.Bar(theme=theme)
|
||||
dualScreen = len(bar.children) > 1
|
||||
leftPreferred = 0 if dualScreen else None
|
||||
rightPreferred = 1 if dualScreen else None
|
||||
dual_screen = len(bar.children) > 1
|
||||
left_preferred = 0 if dual_screen else None
|
||||
right_preferred = 1 if dual_screen else None
|
||||
|
||||
workspaces_suffixes = "▲■"
|
||||
workspaces_names = dict(
|
||||
(str(i + 1), f"{i+1} {c}") for i, c in enumerate(workspaces_suffixes)
|
||||
)
|
||||
workspaces_names = {
|
||||
str(i + 1): f"{i+1} {c}" for i, c in enumerate(workspaces_suffixes)
|
||||
}
|
||||
|
||||
color = rich.color.Color.parse
|
||||
|
||||
bar.addProvider(
|
||||
bar.add_provider(
|
||||
frobar.providers.I3ModeProvider(color=color("red")),
|
||||
alignment=Alignment.LEFT,
|
||||
)
|
||||
bar.addProvider(
|
||||
bar.add_provider(
|
||||
frobar.providers.I3WorkspacesProvider(custom_names=workspaces_names),
|
||||
alignment=Alignment.LEFT,
|
||||
)
|
||||
|
||||
if dualScreen:
|
||||
bar.addProvider(
|
||||
if dual_screen:
|
||||
bar.add_provider(
|
||||
frobar.providers.I3WindowTitleProvider(color=color("white")),
|
||||
screenNum=0,
|
||||
screen_num=0,
|
||||
alignment=Alignment.CENTER,
|
||||
)
|
||||
bar.addProvider(
|
||||
bar.add_provider(
|
||||
frobar.providers.MprisProvider(color=color("bright_white")),
|
||||
screenNum=rightPreferred,
|
||||
screen_num=right_preferred,
|
||||
alignment=Alignment.CENTER,
|
||||
)
|
||||
else:
|
||||
bar.addProvider(
|
||||
bar.add_provider(
|
||||
frobar.common.SpacerProvider(),
|
||||
alignment=Alignment.LEFT,
|
||||
)
|
||||
bar.addProvider(
|
||||
bar.add_provider(
|
||||
frobar.providers.MprisProvider(color=color("bright_white")),
|
||||
alignment=Alignment.LEFT,
|
||||
)
|
||||
|
||||
bar.addProvider(
|
||||
bar.add_provider(
|
||||
frobar.providers.CpuProvider(),
|
||||
screenNum=leftPreferred,
|
||||
screen_num=left_preferred,
|
||||
alignment=Alignment.RIGHT,
|
||||
)
|
||||
bar.addProvider(
|
||||
bar.add_provider(
|
||||
frobar.providers.LoadProvider(),
|
||||
screenNum=leftPreferred,
|
||||
screen_num=left_preferred,
|
||||
alignment=Alignment.RIGHT,
|
||||
)
|
||||
bar.addProvider(
|
||||
bar.add_provider(
|
||||
frobar.providers.RamProvider(),
|
||||
screenNum=leftPreferred,
|
||||
screen_num=left_preferred,
|
||||
alignment=Alignment.RIGHT,
|
||||
)
|
||||
bar.addProvider(
|
||||
bar.add_provider(
|
||||
frobar.providers.TemperatureProvider(),
|
||||
screenNum=leftPreferred,
|
||||
screen_num=left_preferred,
|
||||
alignment=Alignment.RIGHT,
|
||||
)
|
||||
bar.addProvider(
|
||||
bar.add_provider(
|
||||
frobar.providers.BatteryProvider(),
|
||||
screenNum=leftPreferred,
|
||||
screen_num=left_preferred,
|
||||
alignment=Alignment.RIGHT,
|
||||
)
|
||||
bar.addProvider(
|
||||
bar.add_provider(
|
||||
frobar.providers.PulseaudioProvider(color=color("magenta")),
|
||||
screenNum=rightPreferred,
|
||||
screen_num=right_preferred,
|
||||
alignment=Alignment.RIGHT,
|
||||
)
|
||||
bar.addProvider(
|
||||
bar.add_provider(
|
||||
frobar.providers.NetworkProvider(color=color("blue")),
|
||||
screenNum=leftPreferred,
|
||||
screen_num=left_preferred,
|
||||
alignment=Alignment.RIGHT,
|
||||
)
|
||||
bar.addProvider(
|
||||
bar.add_provider(
|
||||
frobar.providers.TimeProvider(color=color("cyan")),
|
||||
alignment=Alignment.RIGHT,
|
||||
)
|
||||
|
|
|
@ -30,15 +30,16 @@ Sortable = str | int
|
|||
|
||||
# Display utilities
|
||||
|
||||
UNIT_THRESHOLD = 1000
|
||||
FLOAT_THRESHOLD = 10
|
||||
|
||||
def humanSize(numi: int) -> str:
|
||||
"""
|
||||
Returns a string of width 3+3
|
||||
"""
|
||||
|
||||
def human_size(numi: int) -> str:
|
||||
"""Return a string of width 3+3."""
|
||||
num = float(numi)
|
||||
for unit in ("B ", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB"):
|
||||
if abs(num) < 1000:
|
||||
if num >= 10:
|
||||
if abs(num) < UNIT_THRESHOLD:
|
||||
if num >= FLOAT_THRESHOLD:
|
||||
return f"{int(num):3d}{unit}"
|
||||
return f"{num:.1f}{unit}"
|
||||
num /= 1024
|
||||
|
@ -64,46 +65,47 @@ def clip(text: str, length: int = 30) -> str:
|
|||
class ComposableText(typing.Generic[P, C]):
|
||||
def __init__(
|
||||
self,
|
||||
parent: typing.Optional[P] = None,
|
||||
sortKey: Sortable = 0,
|
||||
parent: P | None = None,
|
||||
sort_key: Sortable = 0,
|
||||
) -> None:
|
||||
self.parent: typing.Optional[P] = None
|
||||
self.children: typing.MutableSequence[C] = list()
|
||||
self.sortKey = sortKey
|
||||
self.parent: P | None = None
|
||||
self.children: typing.MutableSequence[C] = []
|
||||
self.sortKey = sort_key
|
||||
if parent:
|
||||
self.setParent(parent)
|
||||
self.bar = self.getFirstParentOfType(Bar)
|
||||
self.set_parent(parent)
|
||||
self.bar = self.get_first_parent_of_type(Bar)
|
||||
|
||||
def setParent(self, parent: P) -> None:
|
||||
def set_parent(self, parent: P) -> None:
|
||||
assert self.parent is None
|
||||
parent.children.append(self)
|
||||
assert isinstance(parent.children, list)
|
||||
parent.children.sort(key=lambda c: c.sortKey)
|
||||
self.parent = parent
|
||||
self.parent.updateMarkup()
|
||||
self.parent.update_markup()
|
||||
|
||||
def unsetParent(self) -> None:
|
||||
def unset_parent(self) -> None:
|
||||
assert self.parent
|
||||
self.parent.children.remove(self)
|
||||
self.parent.updateMarkup()
|
||||
self.parent.update_markup()
|
||||
self.parent = None
|
||||
|
||||
def getFirstParentOfType(self, typ: typing.Type[T]) -> T:
|
||||
def get_first_parent_of_type(self, typ: type[T]) -> T:
|
||||
parent = self
|
||||
while not isinstance(parent, typ):
|
||||
assert parent.parent, f"{self} doesn't have a parent of {typ}"
|
||||
parent = parent.parent
|
||||
return parent
|
||||
|
||||
def updateMarkup(self) -> None:
|
||||
def update_markup(self) -> None:
|
||||
self.bar.refresh.set()
|
||||
# TODO OPTI See if worth caching the output
|
||||
|
||||
def generateMarkup(self) -> str:
|
||||
raise NotImplementedError(f"{self} cannot generate markup")
|
||||
def generate_markup(self) -> str:
|
||||
msg = f"{self} cannot generate markup"
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
def getMarkup(self) -> str:
|
||||
return self.generateMarkup()
|
||||
def get_markup(self) -> str:
|
||||
return self.generate_markup()
|
||||
|
||||
|
||||
class Button(enum.Enum):
|
||||
|
@ -114,18 +116,19 @@ class Button(enum.Enum):
|
|||
SCROLL_DOWN = "5"
|
||||
|
||||
|
||||
RICH_DEFAULT_COLOR = rich.color.Color.default()
|
||||
|
||||
|
||||
class Section(ComposableText):
|
||||
"""
|
||||
Colorable block separated by chevrons
|
||||
"""
|
||||
"""Colorable block separated by chevrons."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: "Module",
|
||||
sortKey: Sortable = 0,
|
||||
color: rich.color.Color = rich.color.Color.default(),
|
||||
sort_key: Sortable = 0,
|
||||
color: rich.color.Color = RICH_DEFAULT_COLOR,
|
||||
) -> None:
|
||||
super().__init__(parent=parent, sortKey=sortKey)
|
||||
super().__init__(parent=parent, sort_key=sort_key)
|
||||
self.parent: Module
|
||||
self.color = color
|
||||
|
||||
|
@ -134,9 +137,9 @@ class Section(ComposableText):
|
|||
self.targetSize = -1
|
||||
self.size = -1
|
||||
self.animationTask: asyncio.Task | None = None
|
||||
self.actions: dict[Button, str] = dict()
|
||||
self.actions: dict[Button, str] = {}
|
||||
|
||||
def isHidden(self) -> bool:
|
||||
def is_hidden(self) -> bool:
|
||||
return self.size < 0
|
||||
|
||||
# Geometric series, with a cap
|
||||
|
@ -147,29 +150,29 @@ class Section(ComposableText):
|
|||
async def animate(self) -> None:
|
||||
increment = 1 if self.size < self.targetSize else -1
|
||||
loop = asyncio.get_running_loop()
|
||||
frameTime = loop.time()
|
||||
animTime = self.ANIM_A
|
||||
frame_time = loop.time()
|
||||
anim_time = self.ANIM_A
|
||||
skipped = 0
|
||||
|
||||
while self.size != self.targetSize:
|
||||
self.size += increment
|
||||
self.updateMarkup()
|
||||
self.update_markup()
|
||||
|
||||
animTime *= self.ANIM_R
|
||||
animTime = max(self.ANIM_MIN, animTime)
|
||||
frameTime += animTime
|
||||
sleepTime = frameTime - loop.time()
|
||||
anim_time *= self.ANIM_R
|
||||
anim_time = max(self.ANIM_MIN, anim_time)
|
||||
frame_time += anim_time
|
||||
sleep_time = frame_time - loop.time()
|
||||
|
||||
# In case of stress, skip refreshing by not awaiting
|
||||
if sleepTime > 0:
|
||||
if sleep_time > 0:
|
||||
if skipped > 0:
|
||||
log.warning(f"Skipped {skipped} animation frame(s)")
|
||||
log.warning("Skipped %d animation frame(s)", skipped)
|
||||
skipped = 0
|
||||
await asyncio.sleep(sleepTime)
|
||||
await asyncio.sleep(sleep_time)
|
||||
else:
|
||||
skipped += 1
|
||||
|
||||
def setText(self, text: str | None) -> None:
|
||||
def set_text(self, text: str | None) -> None:
|
||||
# OPTI Don't redraw nor reset animation if setting the same text
|
||||
if self.desiredText == text:
|
||||
return
|
||||
|
@ -184,23 +187,23 @@ class Section(ComposableText):
|
|||
self.animationTask.cancel()
|
||||
# OPTI Skip the whole animation task if not required
|
||||
if self.size == self.targetSize:
|
||||
self.updateMarkup()
|
||||
self.update_markup()
|
||||
else:
|
||||
self.animationTask = self.bar.taskGroup.create_task(self.animate())
|
||||
|
||||
def setAction(
|
||||
def set_action(
|
||||
self, button: Button, callback: typing.Callable | None
|
||||
) -> None:
|
||||
if button in self.actions:
|
||||
command = self.actions[button]
|
||||
self.bar.removeAction(command)
|
||||
self.bar.remove_action(command)
|
||||
del self.actions[button]
|
||||
if callback:
|
||||
command = self.bar.addAction(callback)
|
||||
command = self.bar.add_action(callback)
|
||||
self.actions[button] = command
|
||||
|
||||
def generateMarkup(self) -> str:
|
||||
assert not self.isHidden()
|
||||
def generate_markup(self) -> str:
|
||||
assert not self.is_hidden()
|
||||
pad = max(0, self.size - len(self.text))
|
||||
text = self.text[: self.size] + " " * pad
|
||||
for button, command in self.actions.items():
|
||||
|
@ -209,9 +212,7 @@ class Section(ComposableText):
|
|||
|
||||
|
||||
class Module(ComposableText):
|
||||
"""
|
||||
Sections handled by a same updater
|
||||
"""
|
||||
"""Sections handled by a same updater."""
|
||||
|
||||
def __init__(self, parent: "Side") -> None:
|
||||
super().__init__(parent=parent)
|
||||
|
@ -219,21 +220,21 @@ class Module(ComposableText):
|
|||
self.children: typing.MutableSequence[Section]
|
||||
|
||||
self.mirroring: Module | None = None
|
||||
self.mirrors: list[Module] = list()
|
||||
self.mirrors: list[Module] = []
|
||||
|
||||
def mirror(self, module: "Module") -> None:
|
||||
self.mirroring = module
|
||||
module.mirrors.append(self)
|
||||
|
||||
def getSections(self) -> typing.Sequence[Section]:
|
||||
def get_sections(self) -> typing.Sequence[Section]:
|
||||
if self.mirroring:
|
||||
return self.mirroring.children
|
||||
return self.children
|
||||
|
||||
def updateMarkup(self) -> None:
|
||||
super().updateMarkup()
|
||||
def update_markup(self) -> None:
|
||||
super().update_markup()
|
||||
for mirror in self.mirrors:
|
||||
mirror.updateMarkup()
|
||||
mirror.update_markup()
|
||||
|
||||
|
||||
class Alignment(enum.Enum):
|
||||
|
@ -249,39 +250,41 @@ class Side(ComposableText):
|
|||
self.children: typing.MutableSequence[Module] = []
|
||||
|
||||
self.alignment = alignment
|
||||
self.bar = parent.getFirstParentOfType(Bar)
|
||||
self.bar = parent.get_first_parent_of_type(Bar)
|
||||
|
||||
def generateMarkup(self) -> str:
|
||||
def generate_markup(self) -> str:
|
||||
if not self.children:
|
||||
return ""
|
||||
text = "%{" + self.alignment.value + "}"
|
||||
lastSection: Section | None = None
|
||||
last_section: Section | None = None
|
||||
for module in self.children:
|
||||
for section in module.getSections():
|
||||
if section.isHidden():
|
||||
for section in module.get_sections():
|
||||
if section.is_hidden():
|
||||
continue
|
||||
hexa = section.color.get_truecolor(theme=self.bar.theme).hex
|
||||
if lastSection is None:
|
||||
if self.alignment == Alignment.LEFT:
|
||||
text += "%{B" + hexa + "}%{F-}"
|
||||
else:
|
||||
text += "%{B-}%{F" + hexa + "}%{R}%{F-}"
|
||||
elif isinstance(lastSection, SpacerSection):
|
||||
if last_section is None:
|
||||
text += (
|
||||
"%{B" + hexa + "}%{F-}"
|
||||
if self.alignment == Alignment.LEFT
|
||||
else "%{B-}%{F" + hexa + "}%{R}%{F-}"
|
||||
)
|
||||
elif isinstance(last_section, SpacerSection):
|
||||
text += "%{B-}%{F" + hexa + "}%{R}%{F-}"
|
||||
else:
|
||||
if self.alignment == Alignment.RIGHT:
|
||||
if lastSection.color == section.color:
|
||||
text += ""
|
||||
else:
|
||||
text += "%{F" + hexa + "}%{R}"
|
||||
elif lastSection.color == section.color:
|
||||
text += (
|
||||
""
|
||||
if last_section.color == section.color
|
||||
else "%{F" + hexa + "}%{R}"
|
||||
)
|
||||
elif last_section.color == section.color:
|
||||
text += ""
|
||||
else:
|
||||
text += "%{R}%{B" + hexa + "}"
|
||||
text += "%{F-}"
|
||||
text += section.getMarkup()
|
||||
lastSection = section
|
||||
if self.alignment != Alignment.RIGHT and lastSection:
|
||||
text += section.get_markup()
|
||||
last_section = section
|
||||
if self.alignment != Alignment.RIGHT and last_section:
|
||||
text += "%{R}%{B-}"
|
||||
return text
|
||||
|
||||
|
@ -297,32 +300,33 @@ class Screen(ComposableText):
|
|||
for alignment in Alignment:
|
||||
Side(parent=self, alignment=alignment)
|
||||
|
||||
def generateMarkup(self) -> str:
|
||||
def generate_markup(self) -> str:
|
||||
return ("%{Sn" + self.output + "}") + "".join(
|
||||
side.getMarkup() for side in self.children
|
||||
side.get_markup() for side in self.children
|
||||
)
|
||||
|
||||
|
||||
RICH_DEFAULT_THEME = rich.terminal_theme.DEFAULT_TERMINAL_THEME
|
||||
|
||||
|
||||
class Bar(ComposableText):
|
||||
"""
|
||||
Top-level
|
||||
"""
|
||||
"""Top-level."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
theme: rich.terminal_theme.TerminalTheme = rich.terminal_theme.DEFAULT_TERMINAL_THEME,
|
||||
theme: rich.terminal_theme.TerminalTheme = RICH_DEFAULT_THEME,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.parent: None
|
||||
self.children: typing.MutableSequence[Screen]
|
||||
self.longRunningTasks: list[asyncio.Task] = list()
|
||||
self.longRunningTasks: list[asyncio.Task] = []
|
||||
self.theme = theme
|
||||
|
||||
self.refresh = asyncio.Event()
|
||||
self.taskGroup = asyncio.TaskGroup()
|
||||
self.providers: list[Provider] = list()
|
||||
self.providers: list[Provider] = []
|
||||
self.actionIndex = 0
|
||||
self.actions: dict[str, typing.Callable] = dict()
|
||||
self.actions: dict[str, typing.Callable] = {}
|
||||
|
||||
self.periodicProviderTask: typing.Coroutine | None = None
|
||||
|
||||
|
@ -334,7 +338,7 @@ class Bar(ComposableText):
|
|||
continue
|
||||
Screen(parent=self, output=output.name)
|
||||
|
||||
def addLongRunningTask(self, coro: typing.Coroutine) -> None:
|
||||
def add_long_running_task(self, coro: typing.Coroutine) -> None:
|
||||
task = self.taskGroup.create_task(coro)
|
||||
self.longRunningTasks.append(task)
|
||||
|
||||
|
@ -360,10 +364,10 @@ class Bar(ComposableText):
|
|||
while True:
|
||||
await self.refresh.wait()
|
||||
self.refresh.clear()
|
||||
markup = self.getMarkup()
|
||||
markup = self.get_markup()
|
||||
proc.stdin.write(markup.encode())
|
||||
|
||||
async def actionHandler() -> None:
|
||||
async def action_handler() -> None:
|
||||
assert proc.stdout
|
||||
while True:
|
||||
line = await proc.stdout.readline()
|
||||
|
@ -371,39 +375,36 @@ class Bar(ComposableText):
|
|||
callback = self.actions.get(command)
|
||||
if callback is None:
|
||||
# In some conditions on start it's empty
|
||||
log.error(f"Unknown command: {command}")
|
||||
log.error("Unknown command: %s", command)
|
||||
return
|
||||
callback()
|
||||
|
||||
async with self.taskGroup:
|
||||
self.addLongRunningTask(refresher())
|
||||
self.addLongRunningTask(actionHandler())
|
||||
self.add_long_running_task(refresher())
|
||||
self.add_long_running_task(action_handler())
|
||||
for provider in self.providers:
|
||||
self.addLongRunningTask(provider.run())
|
||||
self.add_long_running_task(provider.run())
|
||||
|
||||
def exit() -> None:
|
||||
def finish() -> None:
|
||||
log.info("Terminating")
|
||||
for task in self.longRunningTasks:
|
||||
task.cancel()
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.add_signal_handler(signal.SIGINT, exit)
|
||||
loop.add_signal_handler(signal.SIGINT, finish)
|
||||
|
||||
def generateMarkup(self) -> str:
|
||||
return "".join(screen.getMarkup() for screen in self.children) + "\n"
|
||||
def generate_markup(self) -> str:
|
||||
return "".join(screen.get_markup() for screen in self.children) + "\n"
|
||||
|
||||
def addProvider(
|
||||
def add_provider(
|
||||
self,
|
||||
provider: "Provider",
|
||||
alignment: Alignment = Alignment.LEFT,
|
||||
screenNum: int | None = None,
|
||||
screen_num: int | None = None, # None = all screens
|
||||
) -> None:
|
||||
"""
|
||||
screenNum: the provider will be added on this screen if set, all otherwise
|
||||
"""
|
||||
modules = list()
|
||||
modules = []
|
||||
for s, screen in enumerate(self.children):
|
||||
if screenNum is None or s == screenNum:
|
||||
if screen_num is None or s == screen_num:
|
||||
side = next(
|
||||
filter(lambda s: s.alignment == alignment, screen.children)
|
||||
)
|
||||
|
@ -413,13 +414,13 @@ class Bar(ComposableText):
|
|||
if modules:
|
||||
self.providers.append(provider)
|
||||
|
||||
def addAction(self, callback: typing.Callable) -> str:
|
||||
def add_action(self, callback: typing.Callable) -> str:
|
||||
command = f"{self.actionIndex:x}"
|
||||
self.actions[command] = callback
|
||||
self.actionIndex += 1
|
||||
return command
|
||||
|
||||
def removeAction(self, command: str) -> None:
|
||||
def remove_action(self, command: str) -> None:
|
||||
del self.actions[command]
|
||||
|
||||
def launch(self) -> None:
|
||||
|
@ -431,12 +432,10 @@ class Bar(ComposableText):
|
|||
|
||||
|
||||
class Provider:
|
||||
sectionType: type[Section] = Section
|
||||
section_type: type[Section] = Section
|
||||
|
||||
def __init__(
|
||||
self, color: rich.color.Color = rich.color.Color.default()
|
||||
) -> None:
|
||||
self.modules: list[Module] = list()
|
||||
def __init__(self, color: rich.color.Color = RICH_DEFAULT_COLOR) -> None:
|
||||
self.modules: list[Module] = []
|
||||
self.color = color
|
||||
|
||||
async def run(self) -> None:
|
||||
|
@ -445,9 +444,7 @@ class Provider:
|
|||
|
||||
|
||||
class MirrorProvider(Provider):
|
||||
def __init__(
|
||||
self, color: rich.color.Color = rich.color.Color.default()
|
||||
) -> None:
|
||||
def __init__(self, color: rich.color.Color = RICH_DEFAULT_COLOR) -> None:
|
||||
super().__init__(color=color)
|
||||
self.module: Module
|
||||
|
||||
|
@ -461,19 +458,19 @@ class MirrorProvider(Provider):
|
|||
class SingleSectionProvider(MirrorProvider):
|
||||
async def run(self) -> None:
|
||||
await super().run()
|
||||
self.section = self.sectionType(parent=self.module, color=self.color)
|
||||
self.section = self.section_type(parent=self.module, color=self.color)
|
||||
|
||||
|
||||
class StaticProvider(SingleSectionProvider):
|
||||
def __init__(
|
||||
self, text: str, color: rich.color.Color = rich.color.Color.default()
|
||||
self, text: str, color: rich.color.Color = RICH_DEFAULT_COLOR
|
||||
) -> None:
|
||||
super().__init__(color=color)
|
||||
self.text = text
|
||||
|
||||
async def run(self) -> None:
|
||||
await super().run()
|
||||
self.section.setText(self.text)
|
||||
self.section.set_text(self.text)
|
||||
|
||||
|
||||
class SpacerSection(Section):
|
||||
|
@ -481,7 +478,7 @@ class SpacerSection(Section):
|
|||
|
||||
|
||||
class SpacerProvider(SingleSectionProvider):
|
||||
sectionType = SpacerSection
|
||||
section_type = SpacerSection
|
||||
|
||||
def __init__(self, length: int = 5) -> None:
|
||||
super().__init__(color=rich.color.Color.default())
|
||||
|
@ -490,41 +487,41 @@ class SpacerProvider(SingleSectionProvider):
|
|||
async def run(self) -> None:
|
||||
await super().run()
|
||||
assert isinstance(self.section, SpacerSection)
|
||||
self.section.setText(" " * self.length)
|
||||
self.section.set_text(" " * self.length)
|
||||
|
||||
|
||||
class StatefulSection(Section):
|
||||
def __init__(
|
||||
self,
|
||||
parent: Module,
|
||||
sortKey: Sortable = 0,
|
||||
color: rich.color.Color = rich.color.Color.default(),
|
||||
sort_key: Sortable = 0,
|
||||
color: rich.color.Color = RICH_DEFAULT_COLOR,
|
||||
) -> None:
|
||||
super().__init__(parent=parent, sortKey=sortKey, color=color)
|
||||
super().__init__(parent=parent, sort_key=sort_key, color=color)
|
||||
self.state = 0
|
||||
self.numberStates: int
|
||||
|
||||
self.setAction(Button.CLICK_LEFT, self.incrementState)
|
||||
self.setAction(Button.CLICK_RIGHT, self.decrementState)
|
||||
self.set_action(Button.CLICK_LEFT, self.increment_state)
|
||||
self.set_action(Button.CLICK_RIGHT, self.decrement_state)
|
||||
|
||||
def incrementState(self) -> None:
|
||||
def increment_state(self) -> None:
|
||||
self.state += 1
|
||||
self.changeState()
|
||||
self.change_state()
|
||||
|
||||
def decrementState(self) -> None:
|
||||
def decrement_state(self) -> None:
|
||||
self.state -= 1
|
||||
self.changeState()
|
||||
self.change_state()
|
||||
|
||||
def setChangedState(self, callback: typing.Callable) -> None:
|
||||
def set_changed_state(self, callback: typing.Callable) -> None:
|
||||
self.callback = callback
|
||||
|
||||
def changeState(self) -> None:
|
||||
def change_state(self) -> None:
|
||||
self.state %= self.numberStates
|
||||
self.bar.taskGroup.create_task(self.callback())
|
||||
|
||||
|
||||
class StatefulSectionProvider(Provider):
|
||||
sectionType = StatefulSection
|
||||
section_type = StatefulSection
|
||||
|
||||
|
||||
class SingleStatefulSectionProvider(
|
||||
|
@ -534,46 +531,44 @@ class SingleStatefulSectionProvider(
|
|||
|
||||
|
||||
class MultiSectionsProvider(Provider):
|
||||
def __init__(
|
||||
self, color: rich.color.Color = rich.color.Color.default()
|
||||
) -> None:
|
||||
def __init__(self, color: rich.color.Color = RICH_DEFAULT_COLOR) -> None:
|
||||
super().__init__(color=color)
|
||||
self.sectionKeys: dict[Module, dict[Sortable, Section]] = (
|
||||
collections.defaultdict(dict)
|
||||
)
|
||||
self.updaters: dict[Section, typing.Callable] = dict()
|
||||
self.updaters: dict[Section, typing.Callable] = {}
|
||||
|
||||
async def getSectionUpdater(self, section: Section) -> typing.Callable:
|
||||
async def get_section_updater(self, section: Section) -> typing.Callable:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
async def doNothing() -> None:
|
||||
async def do_nothing() -> None:
|
||||
pass
|
||||
|
||||
async def updateSections(
|
||||
async def update_sections(
|
||||
self, sections: set[Sortable], module: Module
|
||||
) -> None:
|
||||
moduleSections = self.sectionKeys[module]
|
||||
module_sections = self.sectionKeys[module]
|
||||
async with asyncio.TaskGroup() as tg:
|
||||
for sortKey in sections:
|
||||
section = moduleSections.get(sortKey)
|
||||
for sort_key in sections:
|
||||
section = module_sections.get(sort_key)
|
||||
if not section:
|
||||
section = self.sectionType(
|
||||
parent=module, sortKey=sortKey, color=self.color
|
||||
section = self.section_type(
|
||||
parent=module, sort_key=sort_key, color=self.color
|
||||
)
|
||||
self.updaters[section] = await self.getSectionUpdater(
|
||||
self.updaters[section] = await self.get_section_updater(
|
||||
section
|
||||
)
|
||||
moduleSections[sortKey] = section
|
||||
module_sections[sort_key] = section
|
||||
|
||||
updater = self.updaters[section]
|
||||
tg.create_task(updater())
|
||||
|
||||
missingKeys = set(moduleSections.keys()) - sections
|
||||
for missingKey in missingKeys:
|
||||
section = moduleSections.get(missingKey)
|
||||
missing_keys = set(module_sections.keys()) - sections
|
||||
for missing_key in missing_keys:
|
||||
section = module_sections.get(missing_key)
|
||||
assert section
|
||||
section.setText(None)
|
||||
section.set_text(None)
|
||||
|
||||
|
||||
class PeriodicProvider(Provider):
|
||||
|
@ -585,7 +580,7 @@ class PeriodicProvider(Provider):
|
|||
|
||||
@classmethod
|
||||
async def task(cls, bar: Bar) -> None:
|
||||
providers = list()
|
||||
providers = []
|
||||
for provider in bar.providers:
|
||||
if isinstance(provider, PeriodicProvider):
|
||||
providers.append(provider)
|
||||
|
@ -596,7 +591,7 @@ class PeriodicProvider(Provider):
|
|||
loops = [provider.loop() for provider in providers]
|
||||
asyncio.gather(*loops)
|
||||
|
||||
now = datetime.datetime.now()
|
||||
now = datetime.datetime.now(datetime.UTC)
|
||||
# Hardcoded to 1 second... not sure if we want some more than that,
|
||||
# and if the logic to check if a task should run would be a win
|
||||
# compared to the task itself
|
||||
|
@ -606,11 +601,11 @@ class PeriodicProvider(Provider):
|
|||
async def run(self) -> None:
|
||||
await super().run()
|
||||
for module in self.modules:
|
||||
bar = module.getFirstParentOfType(Bar)
|
||||
bar = module.get_first_parent_of_type(Bar)
|
||||
assert bar
|
||||
if not bar.periodicProviderTask:
|
||||
bar.periodicProviderTask = PeriodicProvider.task(bar)
|
||||
bar.addLongRunningTask(bar.periodicProviderTask)
|
||||
bar.add_long_running_task(bar.periodicProviderTask)
|
||||
|
||||
|
||||
class PeriodicStatefulProvider(
|
||||
|
@ -618,7 +613,7 @@ class PeriodicStatefulProvider(
|
|||
):
|
||||
async def run(self) -> None:
|
||||
await super().run()
|
||||
self.section.setChangedState(self.loop)
|
||||
self.section.set_changed_state(self.loop)
|
||||
|
||||
|
||||
class AlertingProvider(Provider):
|
||||
|
@ -626,16 +621,16 @@ class AlertingProvider(Provider):
|
|||
COLOR_WARNING = rich.color.Color.parse("yellow")
|
||||
COLOR_DANGER = rich.color.Color.parse("red")
|
||||
|
||||
warningThreshold: float
|
||||
dangerThreshold: float
|
||||
warning_threshold: float
|
||||
danger_threshold: float
|
||||
|
||||
def updateLevel(self, level: float) -> None:
|
||||
if level > self.dangerThreshold:
|
||||
def update_level(self, level: float) -> None:
|
||||
if level > self.danger_threshold:
|
||||
color = self.COLOR_DANGER
|
||||
elif level > self.warningThreshold:
|
||||
elif level > self.warning_threshold:
|
||||
color = self.COLOR_WARNING
|
||||
else:
|
||||
color = self.COLOR_NORMAL
|
||||
for module in self.modules:
|
||||
for section in module.getSections():
|
||||
for section in module.get_sections():
|
||||
section.color = color
|
||||
|
|
|
@ -16,6 +16,7 @@ import pulsectl_asyncio
|
|||
import rich.color
|
||||
|
||||
from frobar.common import (
|
||||
RICH_DEFAULT_COLOR,
|
||||
AlertingProvider,
|
||||
Button,
|
||||
MirrorProvider,
|
||||
|
@ -29,18 +30,20 @@ from frobar.common import (
|
|||
StatefulSection,
|
||||
StatefulSectionProvider,
|
||||
clip,
|
||||
humanSize,
|
||||
human_size,
|
||||
log,
|
||||
ramp,
|
||||
)
|
||||
|
||||
gi.require_version("Playerctl", "2.0")
|
||||
import gi.repository.Playerctl
|
||||
import gi.repository.Playerctl # noqa: E402
|
||||
|
||||
T = typing.TypeVar("T")
|
||||
|
||||
|
||||
class I3ModeProvider(SingleSectionProvider):
|
||||
def on_mode(self, i3: i3ipc.Connection, e: i3ipc.Event) -> None:
|
||||
self.section.setText(None if e.change == "default" else e.change)
|
||||
def on_mode(self, _i3: i3ipc.Connection, e: i3ipc.Event) -> None:
|
||||
self.section.set_text(None if e.change == "default" else e.change)
|
||||
|
||||
async def run(self) -> None:
|
||||
await super().run()
|
||||
|
@ -54,11 +57,11 @@ class I3ModeProvider(SingleSectionProvider):
|
|||
class I3WindowTitleProvider(SingleSectionProvider):
|
||||
# TODO FEAT To make this available from start, we need to find the
|
||||
# `focused=True` element following the `focus` array
|
||||
def on_window(self, i3: i3ipc.Connection, e: i3ipc.Event) -> None:
|
||||
def on_window(self, _i3: i3ipc.Connection, e: i3ipc.Event) -> None:
|
||||
if e.container.name is None:
|
||||
self.section.setText(None)
|
||||
self.section.set_text(None)
|
||||
else:
|
||||
self.section.setText(clip(e.container.name, 60))
|
||||
self.section.set_text(clip(e.container.name, 60))
|
||||
|
||||
async def run(self) -> None:
|
||||
await super().run()
|
||||
|
@ -76,15 +79,15 @@ class I3WorkspacesProvider(MultiSectionsProvider):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
custom_names: dict[str, str] = {},
|
||||
custom_names: dict[str, str] | None = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.workspaces: dict[int, i3ipc.WorkspaceReply]
|
||||
self.custom_names = custom_names
|
||||
self.custom_names = custom_names or {}
|
||||
|
||||
self.modulesFromOutput: dict[str, Module] = dict()
|
||||
self.modulesFromOutput: dict[str, Module] = {}
|
||||
|
||||
async def getSectionUpdater(self, section: Section) -> typing.Callable:
|
||||
async def get_section_updater(self, section: Section) -> typing.Callable:
|
||||
assert isinstance(section.sortKey, int)
|
||||
num = section.sortKey
|
||||
|
||||
|
@ -93,13 +96,13 @@ class I3WorkspacesProvider(MultiSectionsProvider):
|
|||
self.i3.command(f"workspace number {num}")
|
||||
)
|
||||
|
||||
section.setAction(Button.CLICK_LEFT, switch_to_workspace)
|
||||
section.set_action(Button.CLICK_LEFT, switch_to_workspace)
|
||||
|
||||
async def update() -> None:
|
||||
workspace = self.workspaces.get(num)
|
||||
if workspace is None:
|
||||
log.warning(f"Can't find workspace {num}")
|
||||
section.setText("X")
|
||||
section.set_text("X")
|
||||
return
|
||||
|
||||
name = workspace.name
|
||||
|
@ -113,20 +116,18 @@ class I3WorkspacesProvider(MultiSectionsProvider):
|
|||
section.color = self.COLOR_DEFAULT
|
||||
if workspace.focused:
|
||||
name = self.custom_names.get(name, name)
|
||||
section.setText(name)
|
||||
section.set_text(name)
|
||||
|
||||
return update
|
||||
|
||||
async def updateWorkspaces(self) -> None:
|
||||
"""
|
||||
Since the i3 IPC interface cannot really tell you by events
|
||||
when workspaces get invisible or not urgent anymore.
|
||||
Relying on those exclusively would require reimplementing some of i3 logic.
|
||||
Fetching all the workspaces on event looks ugly but is the most maintainable.
|
||||
Times I tried to challenge this and failed: 2.
|
||||
"""
|
||||
async def update_workspaces(self) -> None:
|
||||
# Since the i3 IPC interface cannot really tell you by events
|
||||
# when workspaces get invisible or not urgent anymore.
|
||||
# Relying on those exclusively would need reimplementing the i3 logic.
|
||||
# Fetching all the workspaces on event is ugly but maintainable.
|
||||
# Times I tried to challenge this and failed: 2.
|
||||
workspaces = await self.i3.get_workspaces()
|
||||
self.workspaces = dict()
|
||||
self.workspaces = {}
|
||||
modules = collections.defaultdict(set)
|
||||
for workspace in workspaces:
|
||||
self.workspaces[workspace.num] = workspace
|
||||
|
@ -135,38 +136,40 @@ class I3WorkspacesProvider(MultiSectionsProvider):
|
|||
|
||||
await asyncio.gather(
|
||||
*[
|
||||
self.updateSections(nums, module)
|
||||
self.update_sections(nums, module)
|
||||
for module, nums in modules.items()
|
||||
]
|
||||
)
|
||||
|
||||
def onWorkspaceChange(
|
||||
self, i3: i3ipc.Connection, e: i3ipc.Event | None = None
|
||||
def on_workspace_change(
|
||||
self,
|
||||
_i3: i3ipc.Connection,
|
||||
_event: i3ipc.Event | None = None,
|
||||
) -> None:
|
||||
# Cancelling the task doesn't seem to prevent performance double-events
|
||||
self.bar.taskGroup.create_task(self.updateWorkspaces())
|
||||
self.bar.taskGroup.create_task(self.update_workspaces())
|
||||
|
||||
async def run(self) -> None:
|
||||
for module in self.modules:
|
||||
screen = module.getFirstParentOfType(Screen)
|
||||
screen = module.get_first_parent_of_type(Screen)
|
||||
output = screen.output
|
||||
self.modulesFromOutput[output] = module
|
||||
self.bar = module.bar
|
||||
|
||||
self.i3 = await i3ipc.aio.Connection(auto_reconnect=True).connect()
|
||||
self.i3.on(i3ipc.Event.WORKSPACE, self.onWorkspaceChange)
|
||||
self.onWorkspaceChange(self.i3)
|
||||
self.i3.on(i3ipc.Event.WORKSPACE, self.on_workspace_change)
|
||||
self.on_workspace_change(self.i3)
|
||||
await self.i3.main()
|
||||
|
||||
|
||||
class MprisProvider(MirrorProvider):
|
||||
STATUSES = {
|
||||
STATUSES: typing.ClassVar = {
|
||||
gi.repository.Playerctl.PlaybackStatus.PLAYING: "",
|
||||
gi.repository.Playerctl.PlaybackStatus.PAUSED: "",
|
||||
gi.repository.Playerctl.PlaybackStatus.STOPPED: "",
|
||||
}
|
||||
|
||||
PROVIDERS = {
|
||||
PROVIDERS: typing.ClassVar = {
|
||||
"mpd": "",
|
||||
"firefox": "",
|
||||
"chromium": "",
|
||||
|
@ -175,10 +178,10 @@ class MprisProvider(MirrorProvider):
|
|||
|
||||
async def run(self) -> None:
|
||||
await super().run()
|
||||
self.status = self.sectionType(parent=self.module, color=self.color)
|
||||
self.album = self.sectionType(parent=self.module, color=self.color)
|
||||
self.artist = self.sectionType(parent=self.module, color=self.color)
|
||||
self.title = self.sectionType(parent=self.module, color=self.color)
|
||||
self.status = self.section_type(parent=self.module, color=self.color)
|
||||
self.album = self.section_type(parent=self.module, color=self.color)
|
||||
self.artist = self.section_type(parent=self.module, color=self.color)
|
||||
self.title = self.section_type(parent=self.module, color=self.color)
|
||||
|
||||
self.manager = gi.repository.Playerctl.PlayerManager()
|
||||
self.manager.connect("name-appeared", self.on_name_appeared)
|
||||
|
@ -196,13 +199,13 @@ class MprisProvider(MirrorProvider):
|
|||
for name in self.manager.props.player_names:
|
||||
self.init_player(name)
|
||||
|
||||
self.updateSections()
|
||||
self.update_sections()
|
||||
|
||||
while True:
|
||||
# Occasionally it will skip a second
|
||||
# but haven't managed to reproduce with debug info
|
||||
await self.playing.wait()
|
||||
self.updateTitle()
|
||||
self.update_title()
|
||||
if self.player:
|
||||
pos = self.player.props.position
|
||||
rem = 1 - (pos % 1000000) / 1000000
|
||||
|
@ -214,20 +217,20 @@ class MprisProvider(MirrorProvider):
|
|||
def get(
|
||||
something: gi.overrides.GLib.Variant,
|
||||
key: str,
|
||||
default: typing.Any = None,
|
||||
) -> typing.Any:
|
||||
if key in something.keys():
|
||||
default: T | None = None,
|
||||
) -> T | None:
|
||||
if key in something.keys(): # noqa: SIM118
|
||||
return something[key]
|
||||
return default
|
||||
|
||||
@staticmethod
|
||||
def formatUs(ms: int) -> str:
|
||||
def format_us(ms: int) -> str:
|
||||
if ms < 60 * 60 * 1000000:
|
||||
return time.strftime("%M:%S", time.gmtime(ms // 1000000))
|
||||
return str(datetime.timedelta(microseconds=ms))
|
||||
|
||||
def findCurrentPlayer(self) -> None:
|
||||
for name in [self.playerctldName] + self.manager.props.player_names:
|
||||
def find_current_player(self) -> None:
|
||||
for name in [self.playerctldName, *self.manager.props.player_names]:
|
||||
try:
|
||||
self.player = gi.repository.Playerctl.Player.new_from_name(
|
||||
name
|
||||
|
@ -241,21 +244,21 @@ class MprisProvider(MirrorProvider):
|
|||
else:
|
||||
self.player = None
|
||||
|
||||
def updateSections(self) -> None:
|
||||
self.findCurrentPlayer()
|
||||
def update_sections(self) -> None:
|
||||
self.find_current_player()
|
||||
|
||||
if self.player is None:
|
||||
self.status.setText(None)
|
||||
self.album.setText(None)
|
||||
self.artist.setText(None)
|
||||
self.title.setText(None)
|
||||
self.status.set_text(None)
|
||||
self.album.set_text(None)
|
||||
self.artist.set_text(None)
|
||||
self.title.set_text(None)
|
||||
self.playing.clear()
|
||||
return
|
||||
|
||||
player = self.player.props.player_name
|
||||
player = self.PROVIDERS.get(player, player)
|
||||
status = self.STATUSES.get(self.player.props.playback_status, "?")
|
||||
self.status.setText(f"{player} {status}")
|
||||
self.status.set_text(f"{player} {status}")
|
||||
|
||||
if (
|
||||
self.player.props.playback_status
|
||||
|
@ -269,48 +272,48 @@ class MprisProvider(MirrorProvider):
|
|||
|
||||
album = self.get(metadata, "xesam:album")
|
||||
if album:
|
||||
self.album.setText(f" {clip(album)}")
|
||||
self.album.set_text(f" {clip(album)}")
|
||||
else:
|
||||
self.album.setText(None)
|
||||
self.album.set_text(None)
|
||||
|
||||
artists = self.get(metadata, "xesam:artist")
|
||||
if artists:
|
||||
artist = ", ".join(artists)
|
||||
self.artist.setText(f" {clip(artist)}")
|
||||
self.artist.set_text(f" {clip(artist)}")
|
||||
else:
|
||||
self.artist.setText(None)
|
||||
self.artist.set_text(None)
|
||||
|
||||
self.updateTitle()
|
||||
self.update_title()
|
||||
|
||||
def updateTitle(self) -> None:
|
||||
def update_title(self) -> None:
|
||||
if self.player is None:
|
||||
return
|
||||
|
||||
metadata = self.player.props.metadata
|
||||
pos = self.player.props.position # In µs
|
||||
text = f" {self.formatUs(pos)}"
|
||||
text = f" {self.format_us(pos)}"
|
||||
dur = self.get(metadata, "mpris:length")
|
||||
if dur:
|
||||
text += f"/{self.formatUs(dur)}"
|
||||
text += f"/{self.format_us(dur)}"
|
||||
title = self.get(metadata, "xesam:title")
|
||||
if title:
|
||||
text += f" {clip(title)}"
|
||||
self.title.setText(text)
|
||||
self.title.set_text(text)
|
||||
|
||||
def on_player_vanished(
|
||||
self,
|
||||
manager: gi.repository.Playerctl.PlayerManager,
|
||||
player: gi.repository.Playerctl.Player,
|
||||
_manager: gi.repository.Playerctl.PlayerManager,
|
||||
_player: gi.repository.Playerctl.Player,
|
||||
) -> None:
|
||||
self.updateSections()
|
||||
self.update_sections()
|
||||
|
||||
def on_event(
|
||||
self,
|
||||
player: gi.repository.Playerctl.Player,
|
||||
_: typing.Any,
|
||||
manager: gi.repository.Playerctl.PlayerManager,
|
||||
_player: gi.repository.Playerctl.Player,
|
||||
_event_data: gi.overrides.GLib.Variant,
|
||||
_manager: gi.repository.Playerctl.PlayerManager,
|
||||
) -> None:
|
||||
self.updateSections()
|
||||
self.update_sections()
|
||||
|
||||
def init_player(self, name: gi.repository.Playerctl.PlayerName) -> None:
|
||||
player = gi.repository.Playerctl.Player.new_from_name(name)
|
||||
|
@ -325,136 +328,165 @@ class MprisProvider(MirrorProvider):
|
|||
self.manager.manage_player(player)
|
||||
|
||||
def on_name_appeared(
|
||||
self, manager: gi.repository.Playerctl.PlayerManager, name: str
|
||||
self,
|
||||
_manager: gi.repository.Playerctl.PlayerManager,
|
||||
name: str,
|
||||
) -> None:
|
||||
self.init_player(name)
|
||||
self.updateSections()
|
||||
self.update_sections()
|
||||
|
||||
|
||||
class CpuProvider(AlertingProvider, PeriodicStatefulProvider):
|
||||
STATE_MINIMIZED = 0
|
||||
STATE_AGGREGATED = 1
|
||||
STATE_FULL = 2
|
||||
|
||||
async def init(self) -> None:
|
||||
self.section.numberStates = 3
|
||||
self.warningThreshold = 75
|
||||
self.dangerThreshold = 95
|
||||
self.warning_threshold = 75
|
||||
self.danger_threshold = 95
|
||||
|
||||
async def loop(self) -> None:
|
||||
percent = psutil.cpu_percent(percpu=False)
|
||||
self.updateLevel(percent)
|
||||
self.update_level(percent)
|
||||
|
||||
text = ""
|
||||
if self.section.state >= 2:
|
||||
if self.section.state >= self.STATE_FULL:
|
||||
percents = psutil.cpu_percent(percpu=True)
|
||||
text += " " + "".join([ramp(p / 100) for p in percents])
|
||||
elif self.section.state >= 1:
|
||||
elif self.section.state >= self.STATE_AGGREGATED:
|
||||
text += " " + ramp(percent / 100)
|
||||
self.section.setText(text)
|
||||
self.section.set_text(text)
|
||||
|
||||
|
||||
class LoadProvider(AlertingProvider, PeriodicStatefulProvider):
|
||||
STATE_MINIMIZED = 0
|
||||
STATE_AGGREGATED = 1
|
||||
STATE_FULL = 2
|
||||
|
||||
async def init(self) -> None:
|
||||
self.section.numberStates = 3
|
||||
self.warningThreshold = 5
|
||||
self.dangerThreshold = 10
|
||||
self.warning_threshold = 5
|
||||
self.danger_threshold = 10
|
||||
|
||||
async def loop(self) -> None:
|
||||
load = os.getloadavg()
|
||||
self.updateLevel(load[0])
|
||||
self.update_level(load[0])
|
||||
|
||||
text = ""
|
||||
loads = 3 if self.section.state >= 2 else self.section.state
|
||||
loads = (
|
||||
3 if self.section.state >= self.STATE_FULL else self.section.state
|
||||
)
|
||||
for load_index in range(loads):
|
||||
text += f" {load[load_index]:.2f}"
|
||||
self.section.setText(text)
|
||||
self.section.set_text(text)
|
||||
|
||||
|
||||
class RamProvider(AlertingProvider, PeriodicStatefulProvider):
|
||||
STATE_MINIMIZED = 0
|
||||
STATE_ICON = 1
|
||||
STATE_NUMBER = 2
|
||||
STATE_FULL = 3
|
||||
|
||||
async def init(self) -> None:
|
||||
self.section.numberStates = 4
|
||||
self.warningThreshold = 75
|
||||
self.dangerThreshold = 95
|
||||
self.warning_threshold = 75
|
||||
self.danger_threshold = 95
|
||||
|
||||
async def loop(self) -> None:
|
||||
mem = psutil.virtual_memory()
|
||||
self.updateLevel(mem.percent)
|
||||
self.update_level(mem.percent)
|
||||
|
||||
text = ""
|
||||
if self.section.state >= 1:
|
||||
if self.section.state >= self.STATE_ICON:
|
||||
text += " " + ramp(mem.percent / 100)
|
||||
if self.section.state >= 2:
|
||||
text += humanSize(mem.total - mem.available)
|
||||
if self.section.state >= 3:
|
||||
text += "/" + humanSize(mem.total)
|
||||
self.section.setText(text)
|
||||
if self.section.state >= self.STATE_NUMBER:
|
||||
text += human_size(mem.total - mem.available)
|
||||
if self.section.state >= self.STATE_FULL:
|
||||
text += "/" + human_size(mem.total)
|
||||
self.section.set_text(text)
|
||||
|
||||
|
||||
class TemperatureProvider(AlertingProvider, PeriodicStatefulProvider):
|
||||
RAMP = ""
|
||||
MAIN_TEMPS = ["coretemp", "amdgpu", "cpu_thermal"]
|
||||
MAIN_TEMPS = ("coretemp", "amdgpu", "cpu_thermal")
|
||||
# For Intel, AMD and ARM respectively.
|
||||
|
||||
STATE_MINIMIZED = 0
|
||||
STATE_FULL = 1
|
||||
|
||||
main: str
|
||||
|
||||
async def init(self) -> None:
|
||||
self.section.numberStates = 2
|
||||
|
||||
allTemp = psutil.sensors_temperatures()
|
||||
all_temps = psutil.sensors_temperatures()
|
||||
for main in self.MAIN_TEMPS:
|
||||
if main in allTemp:
|
||||
if main in all_temps:
|
||||
self.main = main
|
||||
break
|
||||
else:
|
||||
raise IndexError("Could not find suitable temperature sensor")
|
||||
msg = "Could not find suitable temperature sensor"
|
||||
raise IndexError(msg)
|
||||
|
||||
temp = allTemp[self.main][0]
|
||||
self.warningThreshold = temp.high or 90.0
|
||||
self.dangerThreshold = temp.critical or 100.0
|
||||
temp = all_temps[self.main][0]
|
||||
self.warning_threshold = temp.high or 90.0
|
||||
self.danger_threshold = temp.critical or 100.0
|
||||
|
||||
async def loop(self) -> None:
|
||||
allTemp = psutil.sensors_temperatures()
|
||||
temp = allTemp[self.main][0]
|
||||
self.updateLevel(temp.current)
|
||||
all_temps = psutil.sensors_temperatures()
|
||||
temp = all_temps[self.main][0]
|
||||
self.update_level(temp.current)
|
||||
|
||||
text = ramp(temp.current / self.warningThreshold, self.RAMP)
|
||||
if self.section.state >= 1:
|
||||
text = ramp(temp.current / self.warning_threshold, self.RAMP)
|
||||
if self.section.state >= self.STATE_FULL:
|
||||
text += f" {temp.current:.0f}°C"
|
||||
self.section.setText(text)
|
||||
self.section.set_text(text)
|
||||
|
||||
|
||||
class BatteryProvider(AlertingProvider, PeriodicStatefulProvider):
|
||||
# TODO Support ACPID for events
|
||||
RAMP = ""
|
||||
|
||||
STATE_MINIMIZED = 0
|
||||
STATE_PERCENTAGE = 1
|
||||
STATE_ESTIMATE = 2
|
||||
|
||||
async def init(self) -> None:
|
||||
self.section.numberStates = 3
|
||||
# TODO 1 refresh rate is too quick
|
||||
|
||||
self.warningThreshold = 75
|
||||
self.dangerThreshold = 95
|
||||
self.warning_threshold = 75
|
||||
self.danger_threshold = 95
|
||||
|
||||
async def loop(self) -> None:
|
||||
bat = psutil.sensors_battery()
|
||||
if not bat:
|
||||
self.section.setText(None)
|
||||
self.section.set_text(None)
|
||||
|
||||
self.updateLevel(100 - bat.percent)
|
||||
self.update_level(100 - bat.percent)
|
||||
|
||||
text = "" if bat.power_plugged else ""
|
||||
text += ramp(bat.percent / 100, self.RAMP)
|
||||
|
||||
if self.section.state >= 1:
|
||||
if self.section.state >= self.STATE_PERCENTAGE:
|
||||
text += f" {bat.percent:.0f}%"
|
||||
if self.section.state >= 2:
|
||||
if self.section.state >= self.STATE_ESTIMATE:
|
||||
h = int(bat.secsleft / 3600)
|
||||
m = int((bat.secsleft - h * 3600) / 60)
|
||||
text += f" ({h:d}:{m:02d})"
|
||||
|
||||
self.section.setText(text)
|
||||
self.section.set_text(text)
|
||||
|
||||
|
||||
class PulseaudioProvider(
|
||||
MirrorProvider, StatefulSectionProvider, MultiSectionsProvider
|
||||
):
|
||||
async def getSectionUpdater(self, section: Section) -> typing.Callable:
|
||||
STATE_MINIMIZED = 0
|
||||
STATE_BAR = 1
|
||||
STATE_PERCENTAGE = 2
|
||||
|
||||
async def get_section_updater(self, section: Section) -> typing.Callable:
|
||||
assert isinstance(section, StatefulSection)
|
||||
assert isinstance(section.sortKey, str)
|
||||
|
||||
|
@ -478,7 +510,7 @@ class PulseaudioProvider(
|
|||
icon = ""
|
||||
|
||||
section.numberStates = 3
|
||||
section.state = 1
|
||||
section.state = self.STATE_BAR
|
||||
|
||||
# TODO Change volume with wheel
|
||||
|
||||
|
@ -491,23 +523,21 @@ class PulseaudioProvider(
|
|||
"frobar-get-volume"
|
||||
) as pulse:
|
||||
vol = await pulse.volume_get_all_chans(sink)
|
||||
if section.state == 1:
|
||||
if section.state == self.STATE_BAR:
|
||||
text += f" {ramp(vol)}"
|
||||
elif section.state == 2:
|
||||
elif section.state == self.STATE_PERCENTAGE:
|
||||
text += f" {vol:.0%}"
|
||||
# TODO Show which is default
|
||||
section.setText(text)
|
||||
section.set_text(text)
|
||||
|
||||
section.setChangedState(updater)
|
||||
section.set_changed_state(updater)
|
||||
|
||||
return updater
|
||||
|
||||
async def update(self) -> None:
|
||||
async with pulsectl_asyncio.PulseAsync("frobar-list-sinks") as pulse:
|
||||
self.sinks = dict(
|
||||
(sink.name, sink) for sink in await pulse.sink_list()
|
||||
)
|
||||
await self.updateSections(set(self.sinks.keys()), self.module)
|
||||
self.sinks = {sink.name: sink for sink in await pulse.sink_list()}
|
||||
await self.update_sections(set(self.sinks.keys()), self.module)
|
||||
|
||||
async def run(self) -> None:
|
||||
await super().run()
|
||||
|
@ -515,7 +545,7 @@ class PulseaudioProvider(
|
|||
async with pulsectl_asyncio.PulseAsync(
|
||||
"frobar-events-listener"
|
||||
) as pulse:
|
||||
async for event in pulse.subscribe_events(
|
||||
async for _ in pulse.subscribe_events(
|
||||
pulsectl.PulseEventMaskEnum.sink
|
||||
):
|
||||
await self.update()
|
||||
|
@ -527,9 +557,15 @@ class NetworkProvider(
|
|||
StatefulSectionProvider,
|
||||
MultiSectionsProvider,
|
||||
):
|
||||
STATE_MINIMIZED = 0
|
||||
STATE_SSID = 1
|
||||
STATE_IP = 2
|
||||
STATE_SPEED = 3
|
||||
STATE_TOTALS = 4
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
color: rich.color.Color = rich.color.Color.default(),
|
||||
color: rich.color.Color = RICH_DEFAULT_COLOR,
|
||||
) -> None:
|
||||
super().__init__(color=color)
|
||||
|
||||
|
@ -539,27 +575,19 @@ class NetworkProvider(
|
|||
self.io_counters = psutil.net_io_counters(pernic=True)
|
||||
|
||||
@staticmethod
|
||||
def getIfaceAttributes(iface: str) -> tuple[bool, str, bool]:
|
||||
def get_iface_attribute(iface: str) -> tuple[bool, str, bool]:
|
||||
relevant = True
|
||||
icon = "?"
|
||||
wifi = False
|
||||
if iface == "lo":
|
||||
relevant = False
|
||||
elif iface.startswith("eth") or iface.startswith("enp"):
|
||||
if "u" in iface:
|
||||
icon = ""
|
||||
else:
|
||||
icon = ""
|
||||
elif iface.startswith("wlan") or iface.startswith("wl"):
|
||||
elif iface.startswith(("eth", "enp")):
|
||||
icon = "" if "u" in iface else ""
|
||||
elif iface.startswith(("wlan", "wl")):
|
||||
icon = ""
|
||||
wifi = True
|
||||
elif (
|
||||
iface.startswith("tun")
|
||||
or iface.startswith("tap")
|
||||
or iface.startswith("wg")
|
||||
):
|
||||
elif iface.startswith(("tun", "tap", "wg")):
|
||||
icon = ""
|
||||
|
||||
elif iface.startswith("docker"):
|
||||
icon = ""
|
||||
elif iface.startswith("veth"):
|
||||
|
@ -569,30 +597,30 @@ class NetworkProvider(
|
|||
|
||||
return relevant, icon, wifi
|
||||
|
||||
async def getSectionUpdater(self, section: Section) -> typing.Callable:
|
||||
async def get_section_updater(self, section: Section) -> typing.Callable:
|
||||
assert isinstance(section, StatefulSection)
|
||||
assert isinstance(section.sortKey, str)
|
||||
iface = section.sortKey
|
||||
|
||||
relevant, icon, wifi = self.getIfaceAttributes(iface)
|
||||
relevant, icon, wifi = self.get_iface_attribute(iface)
|
||||
|
||||
if not relevant:
|
||||
return self.doNothing
|
||||
return self.do_nothing
|
||||
|
||||
section.numberStates = 5 if wifi else 4
|
||||
section.state = 1 if wifi else 0
|
||||
section.state = self.STATE_SSID if wifi else self.STATE_MINIMIZED
|
||||
|
||||
async def update() -> None:
|
||||
assert isinstance(section, StatefulSection)
|
||||
|
||||
if not self.if_stats[iface].isup:
|
||||
section.setText(None)
|
||||
section.set_text(None)
|
||||
return
|
||||
|
||||
text = icon
|
||||
|
||||
state = section.state + (0 if wifi else 1)
|
||||
if wifi and state >= 1: # SSID
|
||||
if wifi and state >= self.STATE_SSID:
|
||||
cmd = ["iwgetid", iface, "--raw"]
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*cmd, stdout=asyncio.subprocess.PIPE
|
||||
|
@ -600,7 +628,7 @@ class NetworkProvider(
|
|||
stdout, stderr = await proc.communicate()
|
||||
text += f" {stdout.decode().strip()}"
|
||||
|
||||
if state >= 2: # Address
|
||||
if state >= self.STATE_IP:
|
||||
for address in self.if_addrs[iface]:
|
||||
if address.family == socket.AF_INET:
|
||||
net = ipaddress.IPv4Network(
|
||||
|
@ -609,23 +637,23 @@ class NetworkProvider(
|
|||
text += f" {address.address}/{net.prefixlen}"
|
||||
break
|
||||
|
||||
if state >= 3: # Speed
|
||||
prevRecv = self.prev_io_counters[iface].bytes_recv
|
||||
if state >= self.STATE_SPEED:
|
||||
prev_recv = self.prev_io_counters[iface].bytes_recv
|
||||
recv = self.io_counters[iface].bytes_recv
|
||||
prevSent = self.prev_io_counters[iface].bytes_sent
|
||||
prev_sent = self.prev_io_counters[iface].bytes_sent
|
||||
sent = self.io_counters[iface].bytes_sent
|
||||
dt = self.time - self.prev_time
|
||||
|
||||
recvDiff = (recv - prevRecv) / dt
|
||||
sentDiff = (sent - prevSent) / dt
|
||||
text += f" ↓{humanSize(recvDiff)}↑{humanSize(sentDiff)}"
|
||||
recv_diff = (recv - prev_recv) / dt
|
||||
sent_diff = (sent - prev_sent) / dt
|
||||
text += f" ↓{human_size(recv_diff)}↑{human_size(sent_diff)}"
|
||||
|
||||
if state >= 4: # Counter
|
||||
text += f" ⇓{humanSize(recv)}⇑{humanSize(sent)}"
|
||||
if state >= self.STATE_TOTALS:
|
||||
text += f" ⇓{human_size(recv)}⇑{human_size(sent)}"
|
||||
|
||||
section.setText(text)
|
||||
section.set_text(text)
|
||||
|
||||
section.setChangedState(update)
|
||||
section.set_changed_state(update)
|
||||
|
||||
return update
|
||||
|
||||
|
@ -643,17 +671,17 @@ class NetworkProvider(
|
|||
self.if_addrs = psutil.net_if_addrs()
|
||||
self.io_counters = psutil.net_io_counters(pernic=True)
|
||||
|
||||
await self.updateSections(set(self.if_stats.keys()), self.module)
|
||||
await self.update_sections(set(self.if_stats.keys()), self.module)
|
||||
|
||||
|
||||
class TimeProvider(PeriodicStatefulProvider):
|
||||
FORMATS = ["%H:%M", "%m-%d %H:%M:%S", "%a %y-%m-%d %H:%M:%S"]
|
||||
FORMATS = ("%H:%M", "%m-%d %H:%M:%S", "%a %y-%m-%d %H:%M:%S")
|
||||
|
||||
async def init(self) -> None:
|
||||
self.section.state = 1
|
||||
self.section.numberStates = len(self.FORMATS)
|
||||
|
||||
async def loop(self) -> None:
|
||||
now = datetime.datetime.now()
|
||||
format = self.FORMATS[self.section.state]
|
||||
self.section.setText(now.strftime(format))
|
||||
now = datetime.datetime.now(datetime.UTC).astimezone()
|
||||
format_ = self.FORMATS[self.section.state]
|
||||
self.section.set_text(now.strftime(format_))
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
import sys # noqa: I001
|
||||
import contextlib
|
||||
import pathlib
|
||||
|
||||
|
||||
# From https://github.com/python/cpython/blob/v3.7.0b5/Lib/site.py#L436
|
||||
|
@ -11,7 +10,7 @@ def register_readline() -> None:
|
|||
|
||||
try:
|
||||
import readline
|
||||
import rlcompleter
|
||||
import rlcompleter # noqa: F401
|
||||
except ImportError:
|
||||
return
|
||||
|
||||
|
@ -23,14 +22,13 @@ def register_readline() -> None:
|
|||
else:
|
||||
readline.parse_and_bind("tab: complete")
|
||||
|
||||
try:
|
||||
# An OSError here could have many causes, but the most likely one
|
||||
# is that there's no .inputrc file (or .editrc file in the case of
|
||||
# Mac OS X + libedit) in the expected location. In that case, we
|
||||
# want to ignore the exception.
|
||||
cm = contextlib.suppress(OSError)
|
||||
with cm:
|
||||
readline.read_init_file()
|
||||
except OSError:
|
||||
# An OSError here could have many causes, but the most likely one
|
||||
# is that there's no .inputrc file (or .editrc file in the case of
|
||||
# Mac OS X + libedit) in the expected location. In that case, we
|
||||
# want to ignore the exception.
|
||||
pass
|
||||
|
||||
if readline.get_current_history_length() == 0:
|
||||
# If no history was loaded, default to .python_history.
|
||||
|
@ -38,14 +36,11 @@ def register_readline() -> None:
|
|||
# each interpreter exit when readline was already configured
|
||||
# through a PYTHONSTARTUP hook, see:
|
||||
# http://bugs.python.org/issue5845#msg198636
|
||||
history = os.path.join(
|
||||
os.path.expanduser("~"), ".cache/python_history"
|
||||
)
|
||||
try:
|
||||
history = pathlib.Path("~/.cache/python_history").expanduser()
|
||||
cm = contextlib.suppress(OSError)
|
||||
with cm:
|
||||
readline.read_history_file(history)
|
||||
except OSError:
|
||||
pass
|
||||
atexit.register(readline.write_history_file, history)
|
||||
|
||||
|
||||
sys.__interactivehook__ = register_readline
|
||||
sys.__interactivehook__ = register_readline # noqa: attr-defined
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
"""
|
||||
Add the networks saved in wireless_networks to wpa_supplicant,
|
||||
without restarting it or touching its config file.
|
||||
Add the networks saved in wireless_networks to wpa_supplicant.
|
||||
|
||||
Does not restart it or touch its config file.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
NETWORKS_FILE = "/etc/keys/wireless_networks.json"
|
||||
NETWORKS_FILE = pathlib.Path("/etc/keys/wireless_networks.json")
|
||||
|
||||
|
||||
def wpa_cli(command: list[str]) -> list[bytes]:
|
||||
|
@ -20,7 +21,7 @@ def wpa_cli(command: list[str]) -> list[bytes]:
|
|||
return lines
|
||||
|
||||
|
||||
network_numbers: dict[str, int] = dict()
|
||||
network_numbers: dict[str, int] = {}
|
||||
networks_tsv = wpa_cli(["list_networks"])
|
||||
networks_tsv.pop(0)
|
||||
for network_line in networks_tsv:
|
||||
|
@ -34,8 +35,8 @@ for network_line in networks_tsv:
|
|||
).decode()
|
||||
network_numbers[ssid] = number
|
||||
|
||||
if os.path.isfile(NETWORKS_FILE):
|
||||
with open(NETWORKS_FILE) as fd:
|
||||
if NETWORKS_FILE.is_file():
|
||||
with NETWORKS_FILE.open() as fd:
|
||||
networks = json.load(fd)
|
||||
|
||||
for network in networks:
|
||||
|
@ -54,4 +55,5 @@ if os.path.isfile(NETWORKS_FILE):
|
|||
value_str = str(value)
|
||||
ret = wpa_cli(["set_network", number_str, key, value_str])
|
||||
if ret[0] != b"OK":
|
||||
raise RuntimeError(f"Couldn't set {key} for {ssid}, got {ret}")
|
||||
msg = f"Couldn't set {key} for {ssid}, got {ret}"
|
||||
raise RuntimeError(msg)
|
||||
|
|
39
os/wireless/import.py
Executable file → Normal file
39
os/wireless/import.py
Executable file → Normal file
|
@ -1,7 +1,4 @@
|
|||
"""
|
||||
Exports Wi-Fi networks configuration stored in pass
|
||||
into a format readable by Nix.
|
||||
"""
|
||||
"""Exports Wi-Fi networks from password store."""
|
||||
|
||||
# TODO EAP ca_cert=/etc/ssl/... probably won't work. Example fix:
|
||||
# builtins.fetchurl {
|
||||
|
@ -11,52 +8,60 @@ into a format readable by Nix.
|
|||
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import subprocess
|
||||
|
||||
import yaml
|
||||
|
||||
# passpy doesn't handle encoding properly, so doing this with calls
|
||||
|
||||
PASSWORD_STORE = os.environ["PASSWORD_STORE_DIR"]
|
||||
PASSWORD_STORE = pathlib.Path(os.environ["PASSWORD_STORE_DIR"])
|
||||
SUBFOLDER = "wifi"
|
||||
|
||||
|
||||
def list_networks() -> list[str]:
|
||||
def list_networks() -> list[pathlib.Path]:
|
||||
paths = []
|
||||
pass_folder = os.path.join(PASSWORD_STORE, SUBFOLDER)
|
||||
pass_folder = PASSWORD_STORE / SUBFOLDER
|
||||
for filename in os.listdir(pass_folder):
|
||||
if not filename.endswith(".gpg"):
|
||||
continue
|
||||
filepath = os.path.join(pass_folder, filename)
|
||||
if not os.path.isfile(filepath):
|
||||
filepath = pass_folder / filename
|
||||
if not filepath.is_file():
|
||||
continue
|
||||
|
||||
file = filename[:-4]
|
||||
path = os.path.join(SUBFOLDER, file)
|
||||
path = pathlib.Path(SUBFOLDER, file)
|
||||
paths.append(path)
|
||||
paths.sort()
|
||||
return paths
|
||||
|
||||
|
||||
networks: list[dict[str, str | list[str] | int]] = list()
|
||||
networks: list[dict[str, str | list[str] | int]] = []
|
||||
for path in list_networks():
|
||||
proc = subprocess.run(["pass", path], stdout=subprocess.PIPE, check=True)
|
||||
proc = subprocess.run(
|
||||
["pass", path], # noqa: S607
|
||||
stdout=subprocess.PIPE,
|
||||
check=True,
|
||||
)
|
||||
|
||||
raw = proc.stdout.decode()
|
||||
split = raw.split("\n")
|
||||
|
||||
password = split[0]
|
||||
data = yaml.safe_load("\n".join(split[1:])) or dict()
|
||||
data = yaml.safe_load("\n".join(split[1:])) or {}
|
||||
|
||||
# Helpers to prevent repetition
|
||||
suffixes = data.pop("suffixes", [""])
|
||||
data.setdefault("key_mgmt", ["WPA-PSK"] if password else ["NONE"])
|
||||
if password:
|
||||
if any(map(lambda m: "PSK" in m.split("-"), data["key_mgmt"])):
|
||||
if any("PSK" in m.split("-") for m in data["key_mgmt"]):
|
||||
data["psk"] = password
|
||||
if any(map(lambda m: "EAP" in m.split("-"), data["key_mgmt"])):
|
||||
if any("EAP" in m.split("-") for m in data["key_mgmt"]):
|
||||
data["password"] = password
|
||||
assert "ssid" in data, f"{path}: Missing SSID"
|
||||
|
||||
if "ssid" in data:
|
||||
msg = f"{path}: Missing SSID"
|
||||
raise KeyError(msg)
|
||||
data.setdefault("disabled", 0)
|
||||
|
||||
for suffix in suffixes:
|
||||
|
@ -64,5 +69,5 @@ for path in list_networks():
|
|||
network["ssid"] += suffix
|
||||
networks.append(network)
|
||||
|
||||
with open("wireless_networks.json", "w") as fd:
|
||||
with pathlib.Path("wireless_networks.json").open("w") as fd:
|
||||
json.dump(networks, fd, indent=4)
|
||||
|
|
|
@ -1,13 +1,32 @@
|
|||
#! /usr/bin/env python2
|
||||
from subprocess import check_output
|
||||
import subprocess
|
||||
|
||||
|
||||
def get_pass(account):
|
||||
return check_output("pass " + account, shell=True).splitlines()[0]
|
||||
|
||||
|
||||
def beep():
|
||||
check_output(
|
||||
"play -n synth sine E4 sine A5 remix 1-2 fade 0.5 1.2 0.5 2",
|
||||
shell=True,
|
||||
def get_pass(account: str) -> str:
|
||||
proc = subprocess.run(
|
||||
["pass", account], # noqa: S607
|
||||
stdout=subprocess.PIPE,
|
||||
check=True,
|
||||
)
|
||||
|
||||
raw = proc.stdout.decode()
|
||||
return raw.splitlines()[0]
|
||||
|
||||
|
||||
def beep() -> None:
|
||||
cmd = [
|
||||
"play",
|
||||
"-n",
|
||||
"synth",
|
||||
"sine",
|
||||
"E4",
|
||||
"sine",
|
||||
"A5",
|
||||
"remix",
|
||||
"1-2",
|
||||
"fade",
|
||||
"0.5",
|
||||
"1.2",
|
||||
"0.5",
|
||||
"2",
|
||||
]
|
||||
subprocess.run(cmd, check=False)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue