diff --git a/hm/desktop/frobar/.dev/new.py b/hm/desktop/frobar/.dev/new.py index 65e6cbe..1290ad7 100644 --- a/hm/desktop/frobar/.dev/new.py +++ b/hm/desktop/frobar/.dev/new.py @@ -292,6 +292,8 @@ 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: @@ -382,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): @@ -391,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): @@ -419,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) @@ -432,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 @@ -470,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: """ @@ -578,14 +626,12 @@ class NetworkProviderSection(StatefulSection): self.numberStates = 5 if self.wifi else 4 self.state = 1 if self.wifi else 0 - async def updateOnStateChange(self) -> None: - while True: - await self.stateChanged.wait() - await self.provider.updateIface(self.iface) + self.setChangedState(self.update) - async def getText(self) -> str | None: + 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 state = self.state + (0 if self.wifi else 1) # SSID @@ -611,84 +657,71 @@ class NetworkProviderSection(StatefulSection): 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 - recvDiff = recv - prevRecv - sentDiff = sent - prevSent dt = self.provider.time - self.provider.prev_time - recvDiff /= dt - sentDiff /= dt + recvDiff = (recv - prevRecv) / dt + sentDiff = (sent - prevSent) / dt text += f" ↓{humanSize(recvDiff)}↑{humanSize(sentDiff)}" if state >= 4: # Counter text += f" ⇓{humanSize(recv)}⇑{humanSize(sent)}" - return text + 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 run(self) -> None: - await super().run() - + async def init(self) -> None: loop = asyncio.get_running_loop() self.time = loop.time() self.io_counters = psutil.net_io_counters(pernic=True) - while True: - # Separate TaskGroup in case it takes longer than one second, - # it doesn't fill the main TaskGroup - 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) + 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) - 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 + 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 - self.module.bar.addLongRunningTask(section.updateOnStateChange()) - 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)) + tg.create_task(section.update()) + for iface, section in self.sections.items(): + if iface not in self.if_stats: + section.setText(None) + + 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: