diff --git a/config/lemonbar/bar.py b/config/lemonbar/bar.py index 00e9210..01ff0c5 100755 --- a/config/lemonbar/bar.py +++ b/config/lemonbar/bar.py @@ -9,10 +9,6 @@ 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 @@ -28,10 +24,10 @@ if __name__ == "__main__": SYSTEM_THEME = 2 DANGER_THEME = FOCUS_THEME CRITICAL_THEME = URGENT_THEME - Bar.addSectionAll(CpuProvider(theme=SYSTEM_THEME, themeDanger=DANGER_THEME, themeCritical=CRITICAL_THEME), BarGroupType.RIGHT) - Bar.addSectionAll(RamProvider(theme=SYSTEM_THEME, themeDanger=DANGER_THEME, themeCritical=CRITICAL_THEME), BarGroupType.RIGHT) - Bar.addSectionAll(TemperatureProvider(theme=SYSTEM_THEME, themeDanger=DANGER_THEME, themeCritical=CRITICAL_THEME), BarGroupType.RIGHT) - Bar.addSectionAll(BatteryProvider(theme=SYSTEM_THEME, themeDanger=DANGER_THEME, themeCritical=CRITICAL_THEME), BarGroupType.RIGHT) + Bar.addSectionAll(CpuProvider(), BarGroupType.RIGHT) + Bar.addSectionAll(RamProvider(), BarGroupType.RIGHT) + Bar.addSectionAll(TemperatureProvider(), BarGroupType.RIGHT) + Bar.addSectionAll(BatteryProvider(), BarGroupType.RIGHT) # Peripherals PERIPHERAL_THEME = 5 diff --git a/config/lemonbar/display.py b/config/lemonbar/display.py index 37556fc..7009f33 100755 --- a/config/lemonbar/display.py +++ b/config/lemonbar/display.py @@ -17,7 +17,6 @@ log = logging.getLogger() # BarGroup strings should be a good compromise) # TODO Use bytes rather than strings # TODO Use default colors of lemonbar sometimes -# TODO Mouse actions # TODO Adapt bar height with font height @@ -144,7 +143,6 @@ class Bar: # Color for empty sections Bar.string += BarGroup.color(*Section.EMPTY) - # print(Bar.string) Bar.process.stdin.write(bytes(Bar.string + '\n', 'utf-8')) Bar.process.stdin.flush() @@ -194,10 +192,6 @@ class BarGroup: def bgColor(color): return "%{B" + (color or '-') + "}" - @staticmethod - def underlineColor(color): - return "%{U" + (color or '-') + "}" - @staticmethod def color(fg, bg): return BarGroup.fgColor(fg) + BarGroup.bgColor(bg) @@ -291,6 +285,9 @@ class Section: THEMES = list() EMPTY = (FGCOLOR, BGCOLOR) + ICON = None + PERSISTENT = False + #: Sections that do not have their destination size sizeChanging = set() updateThread = SectionThread(daemon=True) @@ -321,14 +318,15 @@ class Section: self.curSize = 0 #: Destination text - self.dstText = '' + self.dstText = Text(' ', Text(), ' ') #: Destination size self.dstSize = 0 #: Groups that have this section self.parents = set() - self.actions = set() + self.icon = self.ICON + def __str__(self): try: @@ -355,93 +353,25 @@ class Section: for parent in self.parents: 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] - - bits = [] - clos = [] - for part in parts: - if isinstance(part, str): - for char in part: - bit += char - bits.append(bit) - clos.append(clo) - bit = '' - elif isinstance(part, dict): - newBit = '' - newClo = '' - if 'fgColor' in part: - newBit = newBit + BarGroup.fgColor(part['fgColor']) - newClo = BarGroup.fgColor(Section.THEMES[self.theme][0]) + newClo - if 'bgColor' in part: - newBit = newBit + BarGroup.bgColor(part['bgColor']) - newClo = BarGroup.bgColor(Section.THEMES[self.theme][0]) + newClo - if 'underlineColor' in part: - newBit = newBit + BarGroup.underlineColor(part['underlineColor']) - newClo = BarGroup.underlineColor(None) + newClo - if 'underline' in part: - newBit = newBit + '%{+u}' - newClo = '%{-u}' + newClo - if 'overline' in part: - newBit = newBit + '%{+o}' - newClo = '%{-o}' + 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 - else: - raise RuntimeError() - - return bits, clos - def updateText(self, text): - # TODO FEAT Actions - # TODO OPTI When srcSize == dstSize, maybe the bit array isn't - # needed - oldActions = self.actions.copy() + assert not isinstance(text, list) # Old behaviour, keep it until everything is cleaned + if isinstance(text, str): + text = Text(text) - if len(text): - if isinstance(text, str): - # TODO OPTI This common case - text = [text] + if self.icon: + self.dstText[0] = ' ' + self.icon + ' ' - self.dstBits, self.dstClos = self.parseParts([' '] + text + [' ']) - # TODO FEAT Half-spaces - - self.dstText = ''.join(self.dstBits) - self.dstSize = len(self.dstBits) + if text is None: + self.dstSize = 3 if self.PERSISTENT else 0 else: - self.dstSize = 0 - - for action in oldActions: - self.unregsiterAction(action) + self.dstText[1] = text + self.dstText.setSection(self) + self.dstSize = len(self.dstText) if self.curSize == self.dstSize: - self.curText = self.dstText - self.informParentsTextChanged() + if self.dstSize > 0: + self.curText = str(self.dstText) + self.informParentsTextChanged() else: Section.sizeChanging.add(self) Section.somethingChanged.set() @@ -449,6 +379,8 @@ class Section: def updateTheme(self, theme): assert isinstance(theme, int) assert theme < len(Section.THEMES) + if theme == self.theme: + return self.theme = theme self.informParentsThemeChanged() Section.somethingChanged.set() @@ -482,9 +414,7 @@ class Section: Section.sizeChanging.remove(self) return - closPos = self.curSize-1 - clos = self.dstClos[closPos] if closPos < len(self.dstClos) else '' - self.curText = ''.join(Section.fit(self.dstBits, self.curSize)) + clos + self.curText = self.dstText.text(size=self.curSize, pad=True) self.informParentsTextChanged() @staticmethod @@ -512,13 +442,15 @@ class Section: class StatefulSection(Section): # TODO Allow to temporary expand the section (e.g. when important change) - NUMBER_STATES = 4 + NUMBER_STATES = None def __init__(self, *args, **kwargs): Section.__init__(self, *args, **kwargs) self.state = 0 if hasattr(self, 'onChangeState'): self.onChangeState(self.state) + self.dstText.setDecorators(scrollUp=self.incrementState, + scrollDown=self.decrementState) def incrementState(self): newState = min(self.state + 1, self.NUMBER_STATES - 1) @@ -536,18 +468,9 @@ class StatefulSection(Section): 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 = '?' + COLORABLE_ICON = '?' def __init__(self, theme=None): StatefulSection.__init__(self, theme=theme) @@ -556,59 +479,154 @@ class ColorCountsSection(StatefulSection): counts = self.subfetcher() # Nothing if not len(counts): - return '' + return None # Icon colored elif self.state == 0 and len(counts) == 1: count, color = counts[0] - return [{'fgColor': color, "cont": self.ICON}] + return Text(self.COLORABLE_ICON, fg=color) # Icon elif self.state == 0 and len(counts) > 1: - return self.ICON + return Text(self.COLORABLE_ICON) # Icon + Total elif self.state == 1 and len(counts) > 1: total = sum([count for count, color in counts]) - return [self.ICON, ' ', str(total)] + return Text(self.COLORABLE_ICON, ' ', total) # Icon + Counts else: - text = [self.ICON] + text = Text(self.COLORABLE_ICON) for count, color in counts: - text += [' ', {'fgColor': color, "cont": str(count)}] + text.append(' ', Text(count, fg=color)) return text -if __name__ == '__main__': - Bar.init() - sec = Section(0) - sech = Section(1) - sec1 = Section(2) - sec2 = Section(3) - sec2h = Section(4) - sec3 = Section(5) - Bar.addSectionAll(sec, BarGroupType.LEFT) - Bar.addSectionAll(sech, BarGroupType.LEFT) - Bar.addSectionAll(sec1, BarGroupType.LEFT) - Bar.addSectionAll(sec2, BarGroupType.RIGHT) - Bar.addSectionAll(sec2h, BarGroupType.RIGHT) - Bar.addSectionAll(sec3, BarGroupType.RIGHT) +class Text: + def _setElements(self, elements): + # TODO OPTI Concatenate consecutrive string + self.elements = list(elements) - time.sleep(1) - sec.updateText("A") - time.sleep(1) - sec.updateText("") - time.sleep(1) - sec.updateText("Hello") - sec1.updateText("world!") - sec2.updateText("Salut") - sec2h.updateText("le") - sec3.updateText("monde !") - time.sleep(3) - sech.updateText("the") - sec2h.updateText("") - time.sleep(2) - sec.updateText("") - sech.updateText("") - sec1.updateText("") - sec2.updateText("") - sec2h.updateText("") - sec3.updateText("") - time.sleep(5) + def _setDecorators(self, decorators): + # TODO OPTI Convert no decorator to strings + self.decorators = decorators + self.prefix = None + self.suffix = None + + def __init__(self, *args, **kwargs): + self._setElements(args) + self._setDecorators(kwargs) + self.section = None + + def append(self, *args): + self._setElements(self.elements + list(args)) + + def prepend(self, *args): + self._setElements(list(args) + self.elements) + + def setElements(self, *args): + self._setElements(args) + + def setDecorators(self, **kwargs): + self._setDecorators(kwargs) + + def setSection(self, section): + assert isinstance(section, Section) + self.section = section + for element in self.elements: + if isinstance(element, Text): + element.setSection(section) + + def _genFixs(self): + if self.prefix is not None and self.suffix is not None: + return + + self.prefix = '' + self.suffix = '' + + def nest(prefix, suffix): + self.prefix = self.prefix + '%{' + prefix + '}' + self.suffix = '%{' + suffix + '}' + self.suffix + + def getColor(val): + # TODO Allow themes + assert isinstance(val, str) and len(val) == 7 + return val + + def button(number, function): + handle = Bar.getFunctionHandle(function) + nest('A' + number + ':' + handle.decode() + ':', 'A' + number) + + for key, val in self.decorators.items(): + if key == 'fg': + reset = self.section.THEMES[self.section.theme][0] + nest('F' + getColor(val), 'F' + reset) + elif key == 'bg': + reset = self.section.THEMES[self.section.theme][1] + nest('B' + getColor(val), 'B' + reset) + elif key == "clickLeft": + button('1', val) + elif key == "clickMiddle": + button('2', val) + elif key == "clickRight": + button('3', val) + elif key == "scrollUp": + button('4', val) + elif key == "scrollDown": + button('5', val) + else: + log.warn("Unkown decorator: {}".format(key)) + + def _text(self, size=None, pad=False): + self._genFixs() + curString = self.prefix + curSize = 0 + remSize = size + + for element in self.elements: + if element is None: + continue + elif isinstance(element, Text): + newString, newSize = element._text(size=remSize) + else: + newString = str(element) + if remSize is not None: + newString = newString[:remSize] + newSize = len(newString) + + curString += newString + curSize += newSize + + if remSize is not None: + remSize -= newSize + if remSize <= 0: + break + + curString += self.suffix + + if pad and remSize > 0: + curString += ' ' * remSize + curSize += remSize + + if size is not None: + if pad: + assert size == curSize + else: + assert size >= curSize + return curString, curSize + + def text(self, *args, **kwargs): + string, size = self._text(*args, **kwargs) + return string + + def __str__(self): + # TODO OPTI + return self.text() + + def __len__(self): + # TODO OPTI + string, size = self._text() + return size + + def __getitem__(self, index): + return self.elements[index] + + def __setitem__(self, index, data): + self.elements[index] = data diff --git a/config/lemonbar/providers.py b/config/lemonbar/providers.py index e0cb3b2..b3f641a 100755 --- a/config/lemonbar/providers.py +++ b/config/lemonbar/providers.py @@ -53,25 +53,60 @@ class TimeProvider(StatefulSection, PeriodicUpdater): StatefulSection.__init__(self, theme) self.changeInterval(1) # TODO OPTI When state < 1 -class CpuProvider(Section, PeriodicUpdater): +class AlertLevel(enum.Enum): + NORMAL = 0 + WARNING = 1 + DANGER = 2 + +class AlertingSection(StatefulSection): + # TODO EASE Correct settings for themes + THEMES = {AlertLevel.NORMAL: 2, + AlertLevel.WARNING: 3, + AlertLevel.DANGER: 1} + PERSISTENT = True + + def getLevel(self, quantity): + if quantity > self.dangerThresold: + return AlertLevel.DANGER + elif quantity > self.warningThresold: + return AlertLevel.WARNING + else: + return AlertLevel.NORMAL + + def updateLevel(self, quantity): + self.level = self.getLevel(quantity) + self.updateTheme(self.THEMES[self.level]) + if self.level == AlertLevel.NORMAL: + return + # TODO Temporary update state + + def __init__(self, theme): + StatefulSection.__init__(self, theme) + self.dangerThresold = 0.90 + self.warningThresold = 0.75 + + +class CpuProvider(AlertingSection, PeriodicUpdater): + NUMBER_STATES = 3 + ICON = '' + def fetcher(self): - percents = psutil.cpu_percent(percpu=True) percent = psutil.cpu_percent(percpu=False) - theme = self.themeCritical if percent >= 90 else \ - (self.themeDanger if percent >= 75 else self.themeNormal) - self.updateTheme(theme) - return ' ' + ''.join([Section.ramp(p/100) for p in percents]) + self.updateLevel(percent/100) + if self.state >= 2: + percents = psutil.cpu_percent(percpu=True) + return ''.join([Section.ramp(p/100) for p in percents]) + elif self.state >= 1: + return Section.ramp(percent/100) def __init__(self, theme=None, themeDanger=None, themeCritical=None): + AlertingSection.__init__(self, theme) PeriodicUpdater.__init__(self) - Section.__init__(self, theme) - self.themeNormal = theme or self.theme - self.themeDanger = themeDanger or self.theme - self.themeCritical = themeCritical or self.theme self.changeInterval(1) class RamProvider(Section, PeriodicUpdater): + # TODO Use AlertingSection """ Shows free RAM """ @@ -93,7 +128,7 @@ class RamProvider(Section, PeriodicUpdater): self.changeInterval(1) class TemperatureProvider(Section, PeriodicUpdater): - # TODO FEAT Change color when > high > critical + # TODO Use AlertingSection RAMP = "" @@ -124,7 +159,7 @@ class TemperatureProvider(Section, PeriodicUpdater): class BatteryProvider(Section, PeriodicUpdater): - # TODO Change color when < thresold% + # TODO Use AlertingSection RAMP = "" @@ -194,24 +229,23 @@ class NetworkProviderSection(StatefulSection, Updater): NUMBER_STATES = 4 - def getIcon(self): + def actType(self): + self.ssid = None if self.iface.startswith('eth') or self.iface.startswith('enp'): if 'u' in self.iface: - return [''] + self.icon = '' else: - return [''] + self.icon = '' elif self.iface.startswith('wlan') or self.iface.startswith('wlp'): - if self.state > 0: + self.icon = '' + if self.showSsid: cmd = ["iwgetid", self.iface, "--raw"] p = subprocess.run(cmd, stdout=subprocess.PIPE) - ssid = p.stdout.strip().decode() - return [' {}'.format(ssid)] - else: - return [''] + self.ssid = p.stdout.strip().decode() elif self.iface.startswith('tun') or self.iface.startswith('tap'): - return [''] + self.icon = '' else: - return ['?'] + self.icon = '?' def getAddresses(self): ipv4 = None @@ -224,26 +258,28 @@ class NetworkProviderSection(StatefulSection, Updater): return ipv4, ipv6 def fetcher(self): - text = [] - if self.iface not in self.parent.stats or \ not self.parent.stats[self.iface].isup or \ self.iface.startswith('lo'): - return text + return None # Get addresses ipv4, ipv6 = self.getAddresses() if ipv4 is None and ipv6 is None: - return text + return None - text = self.getIcon() + text = [] + self.actType() + + if self.showSsid and self.ssid: + text.append(self.ssid) if self.showAddress: if ipv4: netStrFull = '{}/{}'.format(ipv4.address, ipv4.netmask) addr = ipaddress.IPv4Network(netStrFull, strict=False) - addrStr = ' {}/{}'.format(ipv4.address, addr.prefixlen) - text += [addrStr] + addrStr = '{}/{}'.format(ipv4.address, addr.prefixlen) + text.append(addrStr) # TODO IPV6 # if ipv6: # text += ' ' + ipv6.address @@ -255,16 +291,18 @@ class NetworkProviderSection(StatefulSection, Updater): - self.parent.prevIO[self.iface].bytes_sent recvDiff /= self.parent.dt sentDiff /= self.parent.dt - text += [' ↓{}↑{}'.format(humanSize(recvDiff), humanSize(sentDiff))] + text.append('↓{}↑{}'.format(humanSize(recvDiff), + humanSize(sentDiff))) if self.showTransfer: - text += [' ⇓{}⇑{}'.format( + text.append('⇓{}⇑{}'.format( humanSize(self.parent.IO[self.iface].bytes_recv), - humanSize(self.parent.IO[self.iface].bytes_sent))] + humanSize(self.parent.IO[self.iface].bytes_sent))) - return text + return ' '.join(text) def onChangeState(self, state): + self.showSsid = state >= 0 self.showAddress = state >= 1 self.showSpeed = state >= 2 self.showTransfer = state >= 3 @@ -307,7 +345,7 @@ class NetworkProvider(Section, PeriodicUpdater): for section in self.sections.values(): section.refreshData() - return '' + return None def addParent(self, parent): self.parents.add(parent) @@ -328,15 +366,13 @@ class SshAgentProvider(PeriodicUpdater): cmd = ["ssh-add", "-l"] proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) if proc.returncode != 0: - return '' - text = [] + return None + text = Text() for line in proc.stdout.split(b'\n'): if not len(line): continue fingerprint = line.split()[1] - text += [{"cont": '', - "fgColor": randomColor(seed=fingerprint) - }] + text.append(Text('', fg=randomColor(seed=fingerprint))) return text def __init__(self): @@ -349,8 +385,8 @@ class GpgAgentProvider(PeriodicUpdater): proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) # proc = subprocess.run(cmd) if proc.returncode != 0: - return '' - text = [] + return None + text = Text() for line in proc.stdout.split(b'\n'): if not len(line) or line == b'OK': continue @@ -358,9 +394,7 @@ class GpgAgentProvider(PeriodicUpdater): if spli[6] != b'1': continue keygrip = spli[2] - text += [{"cont": '', - "fgColor": randomColor(seed=keygrip) - }] + text.append(Text('', fg=randomColor(seed=keygrip))) return text def __init__(self): @@ -368,14 +402,16 @@ class GpgAgentProvider(PeriodicUpdater): self.changeInterval(5) class KeystoreProvider(Section, MergedUpdater): + ICON = '' + def __init__(self, theme=None): - MergedUpdater.__init__(self, SshAgentProvider(), GpgAgentProvider(), prefix=[' ']) + MergedUpdater.__init__(self, SshAgentProvider(), GpgAgentProvider()) Section.__init__(self, theme) class NotmuchUnreadProvider(ColorCountsSection, PeriodicUpdater): # TODO OPTI Transform InotifyUpdater (watching notmuch folder should be # enough) - ICON = '' + COLORABLE_ICON = '' def subfetcher(self): db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY, path=self.dir) @@ -414,7 +450,7 @@ class NotmuchUnreadProvider(ColorCountsSection, PeriodicUpdater): 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 = '' + COLORABLE_ICON = '' def updateCalendarList(self): calendars = sorted(os.listdir(self.dir)) @@ -575,6 +611,7 @@ class I3WorkspacesProvider(Section, I3Updater): self.initialPopulation(parent) class MpdProvider(Section, ThreadedUpdater): + # TODO FEAT More informations and controls MAX_LENGTH = 50 @@ -610,9 +647,7 @@ class MpdProvider(Section, ThreadedUpdater): if len(infosStr) > MpdProvider.MAX_LENGTH: infosStr = infosStr[:MpdProvider.MAX_LENGTH-1] + '…' - text = [" {}".format(infosStr)] - - return text + return " {}".format(infosStr) def loop(self): try: @@ -624,3 +659,4 @@ class MpdProvider(Section, ThreadedUpdater): except BaseException as e: log.error(e, exc_info=True) + diff --git a/config/lemonbar/updaters.py b/config/lemonbar/updaters.py index 1d65b5b..45c1ff0 100755 --- a/config/lemonbar/updaters.py +++ b/config/lemonbar/updaters.py @@ -9,6 +9,7 @@ import time import logging import coloredlogs import i3ipc +from display import Text coloredlogs.install(level='DEBUG', fmt='%(levelname)s %(message)s') log = logging.getLogger() @@ -231,23 +232,17 @@ class MergedUpdater(Updater): # TODO OPTI Do not update until end of periodic batch def fetcher(self): - text = [] + text = Text() for updater in self.updaters: - data = self.texts[updater] - if not len(data): - continue - if isinstance(data, str): - data = [data] - text += data - if not len(text): - return '' - return self.prefix + text + self.suffix + text.append(self.texts[updater]) + # TODO OPTI After Text.__len__ + # if not len(text): + # return None + return text - def __init__(self, *args, prefix=[''], suffix=['']): + def __init__(self, *args): Updater.__init__(self) - self.prefix = prefix - self.suffix = suffix self.updaters = [] self.texts = dict()