Compare commits
4 commits
01934563a5
...
812e5acf6c
Author | SHA1 | Date | |
---|---|---|---|
Geoffrey Frogeye | 812e5acf6c | ||
Geoffrey Frogeye | 768b38b87f | ||
Geoffrey Frogeye | 6644e85c30 | ||
Geoffrey Frogeye | 1ae7d6b447 |
66
flake.lock
66
flake.lock
|
@ -142,11 +142,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1724895876,
|
||||
"narHash": "sha256-GSqAwa00+vRuHbq9O/yRv7Ov7W/pcMLis3HmeHv8a+Q=",
|
||||
"lastModified": 1727531434,
|
||||
"narHash": "sha256-b+GBgCWd2N6pkiTkRZaMFOPztPO4IVTaclYPrQl2uLk=",
|
||||
"owner": "nix-community",
|
||||
"repo": "disko",
|
||||
"rev": "511388d837178979de66d14ca4a2ebd5f7991cd3",
|
||||
"rev": "b709e1cc33fcde71c7db43850a55ebe6449d0959",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -208,11 +208,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1725024810,
|
||||
"narHash": "sha256-ODYRm8zHfLTH3soTFWE452ydPYz2iTvr9T8ftDMUQ3E=",
|
||||
"lastModified": 1725234343,
|
||||
"narHash": "sha256-+ebgonl3NbiKD2UD0x4BszCZQ6sTfL4xioaM49o5B3Y=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "af510d4a62d071ea13925ce41c95e3dec816c01d",
|
||||
"rev": "567b938d64d4b4112ee253b9274472dc3a346eb6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -226,11 +226,11 @@
|
|||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"lastModified": 1726560853,
|
||||
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -328,11 +328,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1720042825,
|
||||
"narHash": "sha256-A0vrUB6x82/jvf17qPCpxaM+ulJnD8YZwH9Ci0BsAzE=",
|
||||
"lastModified": 1726989464,
|
||||
"narHash": "sha256-Vl+WVTJwutXkimwGprnEtXc/s/s8sMuXzqXaspIGlwM=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "e1391fb22e18a36f57e6999c7a9f966dc80ac073",
|
||||
"rev": "2f23fa308a7c067e52dfcc30a0758f47043ec176",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -392,11 +392,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1724994893,
|
||||
"narHash": "sha256-yutISDGg6HUaZqCaa54EcsfTwew3vhNtt/FNXBBo44g=",
|
||||
"lastModified": 1725189302,
|
||||
"narHash": "sha256-IhXok/kwQqtusPsoguQLCHA+h6gKvgdCrkhIaN+kByA=",
|
||||
"owner": "lnl7",
|
||||
"repo": "nix-darwin",
|
||||
"rev": "c8d3157d1f768e382de5526bb38e74d2245cad04",
|
||||
"rev": "7c4b53a7d9f3a3df902b3fddf2ae245ef20ebcda",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -445,11 +445,11 @@
|
|||
"nmd": "nmd"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1721670745,
|
||||
"narHash": "sha256-rjTQ14dqQ90EaHQy4g/mGylrJ1aZJYc3wCXc4A3GHJg=",
|
||||
"lastModified": 1725658585,
|
||||
"narHash": "sha256-P29z4Gt89n5ps1U7+qmIrj0BuRXGZQSIaOe2+tsPgfw=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nix-on-droid",
|
||||
"rev": "248cc0806120fac9214f503dee0eaf0f47740dd0",
|
||||
"rev": "5d88ff2519e4952f8d22472b52c531bb5f1635fc",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -460,11 +460,11 @@
|
|||
},
|
||||
"nixos-hardware": {
|
||||
"locked": {
|
||||
"lastModified": 1724878143,
|
||||
"narHash": "sha256-UjpKo92iZ25M05kgSOw/Ti6VZwpgdlOa73zHj8OcaDk=",
|
||||
"lastModified": 1727665282,
|
||||
"narHash": "sha256-oKtfbQB1MBypqIyzkC8QCQcVGOa1soaXaGgcBIoh14o=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixos-hardware",
|
||||
"rev": "95c3dfe6ef2e96ddc1ccdd7194e3cda02ca9a8ef",
|
||||
"rev": "11c43c830e533dad1be527ecce379fcf994fbbb5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -474,11 +474,11 @@
|
|||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1725001927,
|
||||
"narHash": "sha256-eV+63gK0Mp7ygCR0Oy4yIYSNcum2VQwnZamHxYTNi+M=",
|
||||
"lastModified": 1727672256,
|
||||
"narHash": "sha256-9/79hjQc9+xyH+QxeMcRsA6hDyw6Z9Eo1/oxjvwirLk=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "6e99f2a27d600612004fbd2c3282d614bfee6421",
|
||||
"rev": "1719f27dd95fd4206afb9cec9f415b539978827e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -533,11 +533,11 @@
|
|||
"treefmt-nix": "treefmt-nix"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1725107436,
|
||||
"narHash": "sha256-84Rz+GeFifzaJHnyMlkz4TdnrWxQdryTQKU3XVFQR1Q=",
|
||||
"lastModified": 1725350106,
|
||||
"narHash": "sha256-TaMMlI2KPJ3wCyxJk6AShOLhNuTeabHCnvYRkLBlEFs=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixvim",
|
||||
"rev": "7cae6d0202140ec322e18b65b63d03b423d595f7",
|
||||
"rev": "0f2c31e6a57a83ed4e6fa3adc76749620231055d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -587,11 +587,11 @@
|
|||
},
|
||||
"nur": {
|
||||
"locked": {
|
||||
"lastModified": 1725177927,
|
||||
"narHash": "sha256-l6Wu5dnme8LpkdLYe+/WxKzK5Pgi96Iiuge9wfnzb4E=",
|
||||
"lastModified": 1727779756,
|
||||
"narHash": "sha256-KLeROOi6VYct8lP1TIAPABlKOsisecKZLOozD5W54PE=",
|
||||
"owner": "nix-community",
|
||||
"repo": "NUR",
|
||||
"rev": "90b3a926d1c4d52c2d3851702be75cbde4e13a0f",
|
||||
"rev": "01c22fb0d5c07a4a2f9ffc4b132cfa4ee4e1cce2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -699,11 +699,11 @@
|
|||
},
|
||||
"unixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1725183711,
|
||||
"narHash": "sha256-gkjg8FfjL92azt3gzZUm1+v+U4y+wbQE630uIf4Aybo=",
|
||||
"lastModified": 1727785308,
|
||||
"narHash": "sha256-t2PqANZPqbtiqOjiQFaHEXuMeG1laa1g+4OcQuo+MjE=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a2c345850e5e1d96c62e7fa8ca6c9d77ebad1c37",
|
||||
"rev": "1c9c0eabb80d35c24eb4e7968c9ee15641a3e0fd",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
@ -3,9 +3,11 @@
|
|||
import asyncio
|
||||
import datetime
|
||||
import enum
|
||||
import ipaddress
|
||||
import logging
|
||||
import random
|
||||
import signal
|
||||
import socket
|
||||
import typing
|
||||
|
||||
import coloredlogs
|
||||
|
@ -22,6 +24,21 @@ C = typing.TypeVar("C", bound="ComposableText")
|
|||
Sortable = str | int
|
||||
|
||||
|
||||
def humanSize(numi: int) -> str:
|
||||
"""
|
||||
Returns 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:
|
||||
return "{:3d}{}".format(int(num), unit)
|
||||
else:
|
||||
return "{:.1f}{}".format(num, unit)
|
||||
num /= 1024
|
||||
return "{:d}YiB".format(numi)
|
||||
|
||||
|
||||
class ComposableText(typing.Generic[P, C]):
|
||||
|
||||
def __init__(
|
||||
|
@ -267,6 +284,7 @@ class Bar(ComposableText):
|
|||
super().__init__()
|
||||
self.parent: None
|
||||
self.children: typing.MutableSequence[Screen]
|
||||
self.longRunningTasks: list[asyncio.Task] = list()
|
||||
|
||||
self.refresh = asyncio.Event()
|
||||
self.taskGroup = asyncio.TaskGroup()
|
||||
|
@ -274,12 +292,18 @@ class Bar(ComposableText):
|
|||
self.actionIndex = 0
|
||||
self.actions: dict[str, typing.Callable] = dict()
|
||||
|
||||
self.periodicProviderTask: typing.Coroutine | None = None
|
||||
|
||||
i3 = i3ipc.Connection()
|
||||
for output in i3.get_outputs():
|
||||
if not output.active:
|
||||
continue
|
||||
Screen(parent=self, output=output.name)
|
||||
|
||||
def addLongRunningTask(self, coro: typing.Coroutine) -> None:
|
||||
task = self.taskGroup.create_task(coro)
|
||||
self.longRunningTasks.append(task)
|
||||
|
||||
async def run(self) -> None:
|
||||
cmd = [
|
||||
"lemonbar",
|
||||
|
@ -299,7 +323,6 @@ class Bar(ComposableText):
|
|||
await self.refresh.wait()
|
||||
self.refresh.clear()
|
||||
markup = self.getMarkup()
|
||||
# log.debug(markup)
|
||||
proc.stdin.write(markup.encode())
|
||||
|
||||
async def actionHandler() -> None:
|
||||
|
@ -310,21 +333,15 @@ class Bar(ComposableText):
|
|||
callback = self.actions[command]
|
||||
callback()
|
||||
|
||||
longRunningTasks = list()
|
||||
|
||||
def addLongRunningTask(coro: typing.Coroutine) -> None:
|
||||
task = self.taskGroup.create_task(coro)
|
||||
longRunningTasks.append(task)
|
||||
|
||||
async with self.taskGroup:
|
||||
addLongRunningTask(refresher())
|
||||
addLongRunningTask(actionHandler())
|
||||
self.addLongRunningTask(refresher())
|
||||
self.addLongRunningTask(actionHandler())
|
||||
for provider in self.providers:
|
||||
addLongRunningTask(provider.run())
|
||||
self.addLongRunningTask(provider.run())
|
||||
|
||||
def exit() -> None:
|
||||
log.info("Terminating")
|
||||
for task in longRunningTasks:
|
||||
for task in self.longRunningTasks:
|
||||
task.cancel()
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
@ -367,7 +384,8 @@ class Provider:
|
|||
self.modules: list[Module] = list()
|
||||
|
||||
async def run(self) -> None:
|
||||
raise NotImplementedError()
|
||||
# Not a NotImplementedError, otherwise can't combine all classes
|
||||
pass
|
||||
|
||||
|
||||
class MirrorProvider(Provider):
|
||||
|
@ -376,17 +394,16 @@ class MirrorProvider(Provider):
|
|||
self.module: Module
|
||||
|
||||
async def run(self) -> None:
|
||||
await super().run()
|
||||
self.module = self.modules[0]
|
||||
for module in self.modules[1:]:
|
||||
module.mirror(self.module)
|
||||
|
||||
|
||||
class SingleSectionProvider(MirrorProvider):
|
||||
SECTION_CLASS = Section
|
||||
|
||||
async def run(self) -> None:
|
||||
await super().run()
|
||||
self.section = self.SECTION_CLASS(parent=self.module)
|
||||
self.section = Section(parent=self.module)
|
||||
|
||||
|
||||
class StaticProvider(SingleSectionProvider):
|
||||
|
@ -404,7 +421,6 @@ class StatefulSection(Section):
|
|||
super().__init__(parent=parent, sortKey=sortKey)
|
||||
self.state = 0
|
||||
self.numberStates: int
|
||||
self.stateChanged = asyncio.Event()
|
||||
|
||||
self.setAction(Button.CLICK_LEFT, self.incrementState)
|
||||
self.setAction(Button.CLICK_RIGHT, self.decrementState)
|
||||
|
@ -417,14 +433,61 @@ class StatefulSection(Section):
|
|||
self.state -= 1
|
||||
self.changeState()
|
||||
|
||||
def setChangedState(self, callback: typing.Callable) -> None:
|
||||
self.callback = callback
|
||||
|
||||
def changeState(self) -> None:
|
||||
self.state %= self.numberStates
|
||||
self.stateChanged.set()
|
||||
self.stateChanged.clear()
|
||||
self.bar.taskGroup.create_task(self.callback())
|
||||
|
||||
|
||||
class StatefulProvider(SingleSectionProvider):
|
||||
SECTION_CLASS = StatefulSection
|
||||
class SingleStatefulSectionProvider(MirrorProvider):
|
||||
async def run(self) -> None:
|
||||
await super().run()
|
||||
self.section = StatefulSection(parent=self.module)
|
||||
|
||||
|
||||
class PeriodicProvider(Provider):
|
||||
async def init(self) -> None:
|
||||
pass
|
||||
|
||||
async def loop(self) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
async def task(cls, bar: Bar) -> None:
|
||||
providers = list()
|
||||
for provider in bar.providers:
|
||||
if isinstance(provider, PeriodicProvider):
|
||||
providers.append(provider)
|
||||
await provider.init()
|
||||
|
||||
while True:
|
||||
# TODO Block bar update during the periodic update of the loops
|
||||
loops = [provider.loop() for provider in providers]
|
||||
asyncio.gather(*loops)
|
||||
|
||||
now = datetime.datetime.now()
|
||||
# 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
|
||||
remaining = 1 - now.microsecond / 1000000
|
||||
await asyncio.sleep(remaining)
|
||||
|
||||
async def run(self) -> None:
|
||||
await super().run()
|
||||
for module in self.modules:
|
||||
bar = module.getFirstParentOfType(Bar)
|
||||
assert bar
|
||||
if not bar.periodicProviderTask:
|
||||
bar.periodicProviderTask = PeriodicProvider.task(bar)
|
||||
bar.addLongRunningTask(bar.periodicProviderTask)
|
||||
|
||||
|
||||
class PeriodicStatefulProvider(SingleStatefulSectionProvider, PeriodicProvider):
|
||||
async def run(self) -> None:
|
||||
await super().run()
|
||||
self.section.setChangedState(self.loop)
|
||||
|
||||
|
||||
# Providers
|
||||
|
@ -455,8 +518,8 @@ class I3WindowTitleProvider(SingleSectionProvider):
|
|||
|
||||
|
||||
class I3WorkspacesProvider(Provider):
|
||||
# FIXME Custom names
|
||||
# FIXME Colors
|
||||
# TODO Custom names
|
||||
# TODO Colors
|
||||
|
||||
async def updateWorkspaces(self, i3: i3ipc.Connection) -> None:
|
||||
"""
|
||||
|
@ -563,64 +626,102 @@ class NetworkProviderSection(StatefulSection):
|
|||
self.numberStates = 5 if self.wifi else 4
|
||||
self.state = 1 if self.wifi else 0
|
||||
|
||||
async def getText(self) -> str | None:
|
||||
self.setChangedState(self.update)
|
||||
|
||||
async def update(self) -> None:
|
||||
if self.ignore or not self.provider.if_stats[self.iface].isup:
|
||||
return None
|
||||
|
||||
self.setText(None)
|
||||
return
|
||||
text = self.icon
|
||||
return text
|
||||
|
||||
state = self.state + (0 if self.wifi else 1) # SSID
|
||||
if self.wifi and state >= 1:
|
||||
cmd = ["iwgetid", self.iface, "--raw"]
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*cmd, stdout=asyncio.subprocess.PIPE
|
||||
)
|
||||
stdout, stderr = await proc.communicate()
|
||||
text += f" {stdout.decode().strip()}"
|
||||
|
||||
if state >= 2: # Address
|
||||
for address in self.provider.if_addrs[self.iface]:
|
||||
if address.family == socket.AF_INET:
|
||||
net = ipaddress.IPv4Network(
|
||||
(address.address, address.netmask), strict=False
|
||||
)
|
||||
text += f" {net.with_prefixlen}"
|
||||
break
|
||||
|
||||
if state >= 3: # Speed
|
||||
prevRecv = self.provider.prev_io_counters[self.iface].bytes_recv
|
||||
recv = self.provider.io_counters[self.iface].bytes_recv
|
||||
prevSent = self.provider.prev_io_counters[self.iface].bytes_sent
|
||||
sent = self.provider.io_counters[self.iface].bytes_sent
|
||||
dt = self.provider.time - self.provider.prev_time
|
||||
|
||||
recvDiff = (recv - prevRecv) / dt
|
||||
sentDiff = (sent - prevSent) / dt
|
||||
text += f" ↓{humanSize(recvDiff)}↑{humanSize(sentDiff)}"
|
||||
|
||||
if state >= 4: # Counter
|
||||
text += f" ⇓{humanSize(recv)}⇑{humanSize(sent)}"
|
||||
|
||||
self.setText(text)
|
||||
|
||||
|
||||
class NetworkProvider(MirrorProvider):
|
||||
class NetworkProvider(MirrorProvider, PeriodicProvider):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.sections: dict[str, NetworkProviderSection] = dict()
|
||||
|
||||
async def updateIface(self, iface: str) -> None:
|
||||
section = self.sections[iface]
|
||||
section.setText(await section.getText())
|
||||
async def init(self) -> None:
|
||||
loop = asyncio.get_running_loop()
|
||||
self.time = loop.time()
|
||||
self.io_counters = psutil.net_io_counters(pernic=True)
|
||||
|
||||
async def run(self) -> None:
|
||||
await super().run()
|
||||
async def loop(self) -> None:
|
||||
loop = asyncio.get_running_loop()
|
||||
async with asyncio.TaskGroup() as tg:
|
||||
self.prev_io_counters = self.io_counters
|
||||
self.prev_time = self.time
|
||||
# On-demand would only benefit if_addrs:
|
||||
# stats are used to determine display,
|
||||
# and we want to keep previous io_counters
|
||||
# so displaying stats is ~instant.
|
||||
self.time = loop.time()
|
||||
self.if_stats = psutil.net_if_stats()
|
||||
self.if_addrs = psutil.net_if_addrs()
|
||||
self.io_counters = psutil.net_io_counters(pernic=True)
|
||||
|
||||
while True:
|
||||
# if_addrs: dict[str, list[psutil._common.snicaddr]] = psutil.net_if_addrs()
|
||||
# io_counters: dict[str, psutil._common.snetio] = psutil.net_io_counters(pernic=True)
|
||||
for iface in self.if_stats:
|
||||
section = self.sections.get(iface)
|
||||
if not section:
|
||||
section = NetworkProviderSection(
|
||||
parent=self.module, iface=iface, provider=self
|
||||
)
|
||||
self.sections[iface] = section
|
||||
|
||||
async with asyncio.TaskGroup() as tg:
|
||||
self.if_stats = psutil.net_if_stats()
|
||||
for iface in self.if_stats:
|
||||
if iface not in self.sections:
|
||||
section = NetworkProviderSection(
|
||||
parent=self.module, iface=iface, provider=self
|
||||
)
|
||||
self.sections[iface] = section
|
||||
tg.create_task(section.update())
|
||||
for iface, section in self.sections.items():
|
||||
if iface not in self.if_stats:
|
||||
section.setText(None)
|
||||
|
||||
tg.create_task(self.updateIface(iface))
|
||||
for iface, section in self.sections.items():
|
||||
if iface not in self.if_stats:
|
||||
section.setText(None)
|
||||
tg.create_task(asyncio.sleep(1))
|
||||
async def onStateChange(self, section: StatefulSection) -> None:
|
||||
assert isinstance(section, NetworkProviderSection)
|
||||
await section.update()
|
||||
|
||||
|
||||
class TimeProvider(StatefulProvider):
|
||||
class TimeProvider(PeriodicStatefulProvider):
|
||||
FORMATS = ["%H:%M", "%m-%d %H:%M:%S", "%a %y-%m-%d %H:%M:%S"]
|
||||
|
||||
async def run(self) -> None:
|
||||
await super().run()
|
||||
assert isinstance(self.section, StatefulSection)
|
||||
async def init(self) -> None:
|
||||
self.section.state = 1
|
||||
self.section.numberStates = len(self.FORMATS)
|
||||
|
||||
while True:
|
||||
now = datetime.datetime.now()
|
||||
format = self.FORMATS[self.section.state]
|
||||
self.section.setText(now.strftime(format))
|
||||
|
||||
remaining = 1 - now.microsecond / 1000000
|
||||
try:
|
||||
await asyncio.wait_for(self.section.stateChanged.wait(), remaining)
|
||||
except TimeoutError:
|
||||
pass
|
||||
async def loop(self) -> None:
|
||||
now = datetime.datetime.now()
|
||||
format = self.FORMATS[self.section.state]
|
||||
self.section.setText(now.strftime(format))
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
|
|
Loading…
Reference in a new issue