From ae0c7c7c097fbc9771798b6db6464e7016139d1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Geoffrey=20=E2=80=9CFrogeye=E2=80=9D=20Preud=27homme?= Date: Thu, 6 Sep 2018 12:17:03 +0200 Subject: [PATCH] Bar actions --- config/lemonbar/bar.py | 5 ++ config/lemonbar/display.py | 141 +++++++++++++++++++++++++++++++++-- config/lemonbar/providers.py | 85 ++++++++++----------- 3 files changed, 178 insertions(+), 53 deletions(-) diff --git a/config/lemonbar/bar.py b/config/lemonbar/bar.py index 3871b05..00e9210 100755 --- a/config/lemonbar/bar.py +++ b/config/lemonbar/bar.py @@ -3,11 +3,16 @@ from providers import * # TODO If multiple screen, expand the sections and share them +# TODO Graceful exit if __name__ == "__main__": Bar.init() Updater.init() +# Bar.addSectionAll(NotmuchUnreadProvider(dir='~/.mail/', theme=1), BarGroupType.RIGHT) +# Bar.addSectionAll(TimeProvider(theme=1), BarGroupType.RIGHT) +# else: + WORKSPACE_THEME = 0 FOCUS_THEME = 3 URGENT_THEME = 1 diff --git a/config/lemonbar/display.py b/config/lemonbar/display.py index 4d1a9c5..37556fc 100755 --- a/config/lemonbar/display.py +++ b/config/lemonbar/display.py @@ -4,6 +4,11 @@ import enum import threading import time import subprocess +import logging +import coloredlogs + +coloredlogs.install(level='DEBUG', fmt='%(levelname)s %(message)s') +log = logging.getLogger() # TODO Allow deletion of Bar, BarGroup and Section for screen changes # IDEA Use i3 ipc events rather than relying on xrandr or Xlib (less portable @@ -24,6 +29,19 @@ class BarGroupType(enum.Enum): # MID_RIGHT = 3 +class BarStdoutThread(threading.Thread): + def run(self): + while True: + handle = Bar.process.stdout.readline().strip() + if not len(handle): + continue + if handle not in Bar.actionsH2F: + log.error("Unknown action: {}".format(handle)) + continue + function = Bar.actionsH2F[handle] + function() + + class Bar: """ One bar for each screen @@ -40,7 +58,10 @@ class Bar: cmd = ['lemonbar', '-b'] for font in Bar.FONTS: cmd += ["-f", "{}:size={}".format(font, Bar.FONTSIZE)] - Bar.process = subprocess.Popen(cmd, stdin=subprocess.PIPE) + Bar.process = subprocess.Popen(cmd, stdin=subprocess.PIPE, + stdout=subprocess.PIPE) + Bar.stdoutThread = BarStdoutThread() + Bar.stdoutThread.start() # Debug Bar(0) @@ -51,6 +72,25 @@ class Bar: string = "" process = None + nextHandle = 0 + actionsF2H = dict() + actionsH2F = dict() + + @staticmethod + def getFunctionHandle(function): + assert callable(function) + if function in Bar.actionsF2H.keys(): + return Bar.actionsF2H[function] + + handle = '{:x}'.format(Bar.nextHandle).encode() + Bar.nextHandle += 1 + + Bar.actionsF2H[function] = handle + Bar.actionsH2F[handle] = function + + log.debug("Registered action {} → {}".format(handle, function)) + return handle + @staticmethod def forever(): while True: @@ -288,6 +328,8 @@ class Section: #: Groups that have this section self.parents = set() + self.actions = set() + def __str__(self): try: return "<{}><{}>{:01d}{}{:02d}/{:02d}" \ @@ -314,6 +356,8 @@ class Section: parent.childsTextChanged = True def parseParts(self, parts, bit='', clo=''): + # TODO OPTI Shall benefit of a major refactor, using Objects rather + # than dicts and a stack-based approac if isinstance(parts, str): parts = [parts] @@ -344,10 +388,30 @@ class Section: if 'overline' in part: newBit = newBit + '%{+o}' newClo = '%{-o}' + newClo - newBits, newClos = self.parseParts(part["cont"], bit=newBit, clo=clo+newClo) + if 'mouseLeft' in part: + handle = Bar.getFunctionHandle(part['mouseLeft']) + newBit = newBit + '%{A1:' + handle.decode() + ':}' + newClo = '%{A1}' + newClo + if 'mouseMiddle' in part: + handle = Bar.getFunctionHandle(part['mouseMiddle']) + newBit = newBit + '%{A2:' + handle.decode() + ':}' + newClo = '%{A2}' + newClo + if 'mouseRight' in part: + handle = Bar.getFunctionHandle(part['mouseRight']) + newBit = newBit + '%{A3:' + handle.decode() + ':}' + newClo = '%{A3}' + newClo + if 'mouseScrollUp' in part: + handle = Bar.getFunctionHandle(part['mouseScrollUp']) + newBit = newBit + '%{A4:' + handle.decode() + ':}' + newClo = '%{A4}' + newClo + if 'mouseScrollDown' in part: + handle = Bar.getFunctionHandle(part['mouseScrollDown']) + newBit = newBit + '%{A5:' + handle.decode() + ':}' + newClo = '%{A5}' + newClo + newBits, newClos = self.parseParts(part["cont"], bit=bit+newBit, clo=newClo+clo) + newBits[-1] += newClo # TODO Will sometimes display errors from lemonbar bits += newBits clos += newClos - bit += newClo else: raise RuntimeError() @@ -357,14 +421,14 @@ class Section: # TODO FEAT Actions # TODO OPTI When srcSize == dstSize, maybe the bit array isn't # needed + oldActions = self.actions.copy() + if len(text): if isinstance(text, str): # TODO OPTI This common case text = [text] self.dstBits, self.dstClos = self.parseParts([' '] + text + [' ']) - # TODO BUG Try this and fix some closings that aren't done - # self.dstBits, self.dstClos = self.parseParts(text) # TODO FEAT Half-spaces self.dstText = ''.join(self.dstBits) @@ -372,6 +436,9 @@ class Section: else: self.dstSize = 0 + for action in oldActions: + self.unregsiterAction(action) + if self.curSize == self.dstSize: self.curText = self.dstText self.informParentsTextChanged() @@ -444,9 +511,69 @@ class Section: class StatefulSection(Section): - # TODO Next thing to do # TODO Allow to temporary expand the section (e.g. when important change) - pass + NUMBER_STATES = 4 + + def __init__(self, *args, **kwargs): + Section.__init__(self, *args, **kwargs) + self.state = 0 + if hasattr(self, 'onChangeState'): + self.onChangeState(self.state) + + def incrementState(self): + newState = min(self.state + 1, self.NUMBER_STATES - 1) + self.changeState(newState) + + def decrementState(self): + newState = max(self.state - 1, 0) + self.changeState(newState) + + def changeState(self, state): + assert isinstance(state, int) + assert state < self.NUMBER_STATES + self.state = state + if hasattr(self, 'onChangeState'): + self.onChangeState(state) + self.refreshData() + + def updateText(self, text): + if not len(text): + return text + Section.updateText(self, [{ + "mouseScrollUp": self.incrementState, + "mouseScrollDown": self.decrementState, + "cont": text + }]) + +class ColorCountsSection(StatefulSection): + NUMBER_STATES = 3 + ICON = '?' + + def __init__(self, theme=None): + StatefulSection.__init__(self, theme=theme) + + def fetcher(self): + counts = self.subfetcher() + # Nothing + if not len(counts): + return '' + # Icon colored + elif self.state == 0 and len(counts) == 1: + count, color = counts[0] + return [{'fgColor': color, "cont": self.ICON}] + # Icon + elif self.state == 0 and len(counts) > 1: + return self.ICON + # Icon + Total + elif self.state == 1 and len(counts) > 1: + total = sum([count for count, color in counts]) + return [self.ICON, ' ', str(total)] + # Icon + Counts + else: + text = [self.ICON] + for count, color in counts: + text += [' ', {'fgColor': color, "cont": str(count)}] + return text if __name__ == '__main__': diff --git a/config/lemonbar/providers.py b/config/lemonbar/providers.py index ee7035e..e0cb3b2 100755 --- a/config/lemonbar/providers.py +++ b/config/lemonbar/providers.py @@ -37,16 +37,21 @@ def randomColor(seed=0): return '#{:02x}{:02x}{:02x}'.format(*[random.randint(0, 255) for _ in range(3)]) -class TimeProvider(Section, PeriodicUpdater): +class TimeProvider(StatefulSection, PeriodicUpdater): + + FORMATS = ["%H:%M", + "%d/%m %H:%M:%S", + "%a %d/%m/%y %H:%M:%S"] + NUMBER_STATES = len(FORMATS) + def fetcher(self): now = datetime.datetime.now() - return now.strftime('%d/%m/%y %H:%M:%S') + return now.strftime(self.FORMATS[self.state]) def __init__(self, theme=None): PeriodicUpdater.__init__(self) - Section.__init__(self, theme) - self.changeInterval(1) - + StatefulSection.__init__(self, theme) + self.changeInterval(1) # TODO OPTI When state < 1 class CpuProvider(Section, PeriodicUpdater): def fetcher(self): @@ -185,7 +190,9 @@ class PulseaudioProvider(Section, ThreadedUpdater): self.refreshData() -class NetworkProviderSection(Section, Updater): +class NetworkProviderSection(StatefulSection, Updater): + + NUMBER_STATES = 4 def getIcon(self): if self.iface.startswith('eth') or self.iface.startswith('enp'): @@ -194,11 +201,13 @@ class NetworkProviderSection(Section, Updater): else: return [''] elif self.iface.startswith('wlan') or self.iface.startswith('wlp'): - cmd = ["iwgetid", self.iface, "--raw"] - p = subprocess.run(cmd, stdout=subprocess.PIPE) - - ssid = p.stdout.strip().decode() - return [' {}'.format(ssid)] + if self.state > 0: + cmd = ["iwgetid", self.iface, "--raw"] + p = subprocess.run(cmd, stdout=subprocess.PIPE) + ssid = p.stdout.strip().decode() + return [' {}'.format(ssid)] + else: + return [''] elif self.iface.startswith('tun') or self.iface.startswith('tap'): return [''] else: @@ -255,25 +264,16 @@ class NetworkProviderSection(Section, Updater): return text - def cycleState(self): - newState = (self.state + 1) % 4 - self.changeState(newState) - - def changeState(self, state): - assert isinstance(state, int) - assert state < 4 - self.state = state + def onChangeState(self, state): self.showAddress = state >= 1 self.showSpeed = state >= 2 self.showTransfer = state >= 3 def __init__(self, iface, parent): Updater.__init__(self) - Section.__init__(self, theme=parent.theme) + StatefulSection.__init__(self, theme=parent.theme) self.iface = iface self.parent = parent - self.changeState(1) - self.refreshData() class NetworkProvider(Section, PeriodicUpdater): @@ -372,30 +372,27 @@ class KeystoreProvider(Section, MergedUpdater): MergedUpdater.__init__(self, SshAgentProvider(), GpgAgentProvider(), prefix=[' ']) Section.__init__(self, theme) - -class NotmuchUnreadProvider(Section, PeriodicUpdater): +class NotmuchUnreadProvider(ColorCountsSection, PeriodicUpdater): # TODO OPTI Transform InotifyUpdater (watching notmuch folder should be # enough) - def fetcher(self): + ICON = '' + + def subfetcher(self): db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY, path=self.dir) - text = [] + counts = [] for account in self.accounts: queryStr = 'folder:/{}/ and tag:unread'.format(account) query = notmuch.Query(db, queryStr) nbMsgs = query.count_messages() if nbMsgs < 1: continue - text += [' '] - text += [{"cont": str(nbMsgs), "fgColor": self.colors[account]}] + counts.append((nbMsgs, self.colors[account])) db.close() - if len(text): - return [''] + text - else: - return '' + return counts def __init__(self, dir='~/.mail/', theme=None): PeriodicUpdater.__init__(self) - Section.__init__(self, theme) + ColorCountsSection.__init__(self, theme) self.dir = os.path.realpath(os.path.expanduser(dir)) assert os.path.isdir(self.dir) @@ -414,9 +411,10 @@ class NotmuchUnreadProvider(Section, PeriodicUpdater): self.changeInterval(10) -class TodoProvider(Section, InotifyUpdater): +class TodoProvider(ColorCountsSection, InotifyUpdater): # TODO OPT/UX Maybe we could get more data from the todoman python module # TODO OPT Specific callback for specific directory + ICON = '' def updateCalendarList(self): calendars = sorted(os.listdir(self.dir)) @@ -431,7 +429,7 @@ class TodoProvider(Section, InotifyUpdater): :parm str dir: [main]path value in todoman.conf """ InotifyUpdater.__init__(self) - Section.__init__(self, theme) + ColorCountsSection.__init__(self, theme=theme) self.dir = os.path.realpath(os.path.expanduser(dir)) assert os.path.isdir(self.dir) @@ -456,19 +454,15 @@ class TodoProvider(Section, InotifyUpdater): data = json.loads(proc.stdout) return len(data) - def fetcher(self): - text = [] + def subfetcher(self): + counts = [] self.updateCalendarList() for calendar in self.calendars: c = self.countUndone(calendar) - if c > 0: - color = self.getColor(calendar) - text += [' '] - text += [{"cont": str(c), "fgColor": color}] - if len(text): - return [''] + text - else: - return '' + if c <= 0: + continue + counts.append((c, self.getColor(calendar))) + return counts class I3WindowTitleProvider(Section, I3Updater): # TODO FEAT To make this available from start, we need to find the @@ -630,4 +624,3 @@ class MpdProvider(Section, ThreadedUpdater): except BaseException as e: log.error(e, exc_info=True) -