diff --git a/hm/desktop/frobar/.dev/new.py b/hm/desktop/frobar/.dev/new.py index 9a9b859..52ec70e 100644 --- a/hm/desktop/frobar/.dev/new.py +++ b/hm/desktop/frobar/.dev/new.py @@ -20,27 +20,15 @@ class ComposableText: assert isinstance(prevParent, Bar) self.bar: Bar = prevParent - self.text: str - self.needsComposition = True - - def composeText(self) -> str: - raise NotImplementedError(f"{self} cannot compose text") - - def getText(self) -> str: - if self.needsComposition: - self.text = self.composeText() - print(f"{self} composed {self.text}") - self.needsComposition = False - return self.text - - def setText(self, text: str) -> None: - self.text = text - self.needsComposition = False - parent = self.parent - while parent and not parent.needsComposition: - parent.needsComposition = True - parent = parent.parent + def updateMarkup(self) -> None: self.bar.refresh.set() + # OPTI See if worth caching the output + + def generateMarkup(self) -> str: + raise NotImplementedError(f"{self} cannot generate markup") + + def getMarkup(self) -> str: + return self.generateMarkup() def randomColor(seed: int | bytes | None = None) -> str: @@ -57,6 +45,53 @@ class Section(ComposableText): def __init__(self, parent: "Module") -> None: super().__init__(parent=parent) self.color = randomColor() + self.text: str = "" + self.size = 0 + self.animationTask: asyncio.Task | None = None + + def isHidden(self) -> bool: + return self.text is None + + # Geometric series + ANIM_A = 0.025 + ANIM_R = 0.9 + + async def animate(self) -> None: + targetSize = len(self.text) + increment = 1 if self.size < targetSize else -1 + + loop = asyncio.get_running_loop() + frameTime = loop.time() + animTime = self.ANIM_A + + while self.size != targetSize: + self.size += increment + self.updateMarkup() + + animTime *= self.ANIM_R + frameTime += animTime + sleepTime = frameTime - loop.time() + + # In case of stress, skip refreshing by not awaiting + if sleepTime > 0: + await asyncio.sleep(sleepTime) + + def setText(self, text: str | None) -> None: + # OPTI Skip if same text + oldText = self.text + self.text = f" {text} " + if oldText == self.text: + return + if len(oldText) == len(self.text): + self.updateMarkup() + else: + if self.animationTask: + self.animationTask.cancel() + self.animationTask = self.bar.taskGroup.create_task(self.animate()) + + def generateMarkup(self) -> str: + pad = max(0, self.size - len(self.text)) + return self.text[: self.size] + " " * pad class Module(ComposableText): @@ -68,9 +103,6 @@ class Module(ComposableText): super().__init__(parent=parent) self.sections: list[Section] = [] - def appendSection(self, section: Section) -> None: - self.sections.append(section) - class Alignment(enum.Enum): LEFT = "l" @@ -84,13 +116,15 @@ class Side(ComposableText): self.alignment = alignment self.modules: list[Module] = [] - def composeText(self) -> str: + def generateMarkup(self) -> str: if not self.modules: return "" text = "%{" + self.alignment.value + "}" lastSection: Section | None = None for module in self.modules: for section in module.sections: + if section.isHidden(): + continue if lastSection is None: if self.alignment == Alignment.LEFT: text += "%{B" + section.color + "}%{F-}" @@ -108,9 +142,7 @@ class Side(ComposableText): else: text += "%{R}%{B" + section.color + "}" text += "%{F-}" - text += " " + section.getText() + " " - # FIXME Should be handled by animation - # FIXME Support hidden sections + text += section.getMarkup() lastSection = section if self.alignment != Alignment.RIGHT: text += "%{R}%{B-}" @@ -126,15 +158,12 @@ class Screen(ComposableText): for alignment in Alignment: self.sides[alignment] = Side(parent=self, alignment=alignment) - def composeText(self) -> str: + def generateMarkup(self) -> str: return ("%{Sn" + self.output + "}") + "".join( - side.getText() for side in self.sides.values() + side.getMarkup() for side in self.sides.values() ) -ScreenSelection = enum.Enum("ScreenSelection", ["ALL", "PRIMARY", "SECONDARY"]) - - class Bar(ComposableText): """ Top-level @@ -142,8 +171,9 @@ class Bar(ComposableText): def __init__(self) -> None: super().__init__() - self.providers: list["Provider"] = [] self.refresh = asyncio.Event() + self.taskGroup = asyncio.TaskGroup() + self.providers: list["Provider"] = list() self.screens = [] i3 = i3ipc.Connection() @@ -169,29 +199,32 @@ class Bar(ComposableText): while True: await self.refresh.wait() self.refresh.clear() - proc.stdin.write(self.getText().encode()) + proc.stdin.write(self.getMarkup().encode()) - async with asyncio.TaskGroup() as tg: + async with self.taskGroup as tg: tg.create_task(refresher()) - for updater in self.providers: - tg.create_task(updater.run()) + for provider in self.providers: + tg.create_task(provider.run()) - def composeText(self) -> str: - return "".join(section.getText() for section in self.screens) + "\n" + def generateMarkup(self) -> str: + return "".join(section.getMarkup() for section in self.screens) + "\n" def addProvider( self, provider: "Provider", alignment: Alignment = Alignment.LEFT, - screenSelection: ScreenSelection = ScreenSelection.ALL, + screenNum: int | None = None, + screenCount: int | None = None, ) -> None: - # FIXME Actually have a screenNum and screenCount args + """ + screenNum: the provider will be added on this screen if set, all otherwise + screenCount: the provider will be added if there is this many screens, + always otherwise + """ modules = list() for s, screen in enumerate(self.screens): - if ( - screenSelection == ScreenSelection.ALL - or (screenSelection == ScreenSelection.PRIMARY and s == 0) - or (screenSelection == ScreenSelection.SECONDARY and s == 1) + if (screenCount is None or len(self.screens) == screenCount) and ( + screenNum is None or s == screenNum ): side = screen.sides[alignment] module = Module(parent=side) @@ -220,22 +253,24 @@ class TimeProvider(Provider): # FIXME Allow for mirror(ed) modules so no need for updaters to handle all while True: + now = datetime.datetime.now() for s, section in enumerate(sections): - now = datetime.datetime.now() - section.setText(now.strftime("%a %y-%m-%d %H:%M:%S")) - await asyncio.sleep(1) + # section.setText(now.strftime("%a %y-%m-%d %H:%M:%S.%f")) + section.setText("-" * (now.second % 10)) + remaining = 1 - now.microsecond / 1000000 + await asyncio.sleep(remaining) async def main() -> None: bar = Bar() bar.addProvider(TimeProvider()) - # bar.addProvider(TimeProvider(), screenSelection=ScreenSelection.PRIMARY) - # bar.addProvider(TimeProvider(), alignment=Alignment.CENTER) - # bar.addProvider(TimeProvider(), alignment=Alignment.CENTER) - # bar.addProvider(TimeProvider(), alignment=Alignment.RIGHT) - # bar.addProvider(TimeProvider(), alignment=Alignment.RIGHT) + bar.addProvider(TimeProvider(), screenNum=1) + bar.addProvider(TimeProvider(), alignment=Alignment.CENTER, screenCount=2) + bar.addProvider(TimeProvider(), alignment=Alignment.CENTER) + bar.addProvider(TimeProvider(), alignment=Alignment.RIGHT) + bar.addProvider(TimeProvider(), alignment=Alignment.RIGHT) await bar.run() - # FIXME Why time not updating? asyncio.run(main()) +# TODO Replace while True with while bar.running or something