From 7987cdcaaee08e1dd0be607f8c304706940ca820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Geoffrey=20=E2=80=9CFrogeye=E2=80=9D=20Preud=27homme?= Date: Wed, 5 Sep 2018 09:07:37 +0200 Subject: [PATCH] Bar bar bar --- config/lemonbar/bar.py | 173 ++--------- config/lemonbar/{new.py => display.py} | 105 +++++-- config/lemonbar/net.py | 29 ++ config/lemonbar/oldbar.py | 161 ++++++++++ config/lemonbar/providers.py | 397 +++++++++++++++++++++++++ config/lemonbar/updaters.py | 194 ++++++++++++ config/lemonbar/x.py | 10 + config/polybar/config | 2 +- scripts/remcrlf | 9 +- 9 files changed, 893 insertions(+), 187 deletions(-) rename config/lemonbar/{new.py => display.py} (77%) create mode 100755 config/lemonbar/net.py create mode 100755 config/lemonbar/oldbar.py create mode 100755 config/lemonbar/providers.py create mode 100755 config/lemonbar/updaters.py create mode 100755 config/lemonbar/x.py diff --git a/config/lemonbar/bar.py b/config/lemonbar/bar.py index 2b36ad5..a0e6cdd 100755 --- a/config/lemonbar/bar.py +++ b/config/lemonbar/bar.py @@ -1,161 +1,22 @@ #!/usr/bin/env python3 -""" -Debugging script -""" +from providers import * -import i3ipc -import os -import psutil -# import alsaaudio -from time import time -import subprocess +if __name__ == "__main__": + Bar.init() + Updater.init() -i3 = i3ipc.Connection() -lemonbar = subprocess.Popen(['lemonbar', '-b'], stdin=subprocess.PIPE) + Bar.addSectionAll(I3Provider(), BarGroupType.LEFT) -# Utils -def upChart(p): - block = ' ▁▂▃▄▅▆▇█' - return block[round(p * (len(block)-1))] - -def humanSizeOf(num, suffix='B'): # TODO Credit - for unit in ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']: - if abs(num) < 1024.0: - return "%3.0f%2s%s" % (num, unit, suffix) - num /= 1024.0 - return "%.0f%2s%s" % (num, 'Yi', suffix) - -# Values -mode = '' -container = i3.get_tree().find_focused() -workspaces = i3.get_workspaces() -outputs = i3.get_outputs() - -username = os.environ['USER'] -hostname = os.environ['HOSTNAME'] -if '-' in hostname: - hostname = hostname.split('-')[-1] - -oldNetIO = dict() -oldTime = time() - -def update(): - activeOutputs = sorted(sorted(list(filter(lambda o: o.active, outputs)), key=lambda o: o.rect.y), key=lambda o: o.rect.x) - z = '' - for aOutput in range(len(activeOutputs)): - output = activeOutputs[aOutput] - # Mode || Workspaces - t = [] - if (mode != ''): - t.append(mode) - else: - t.append(' '.join([(w.name.upper() if w.focused else w.name) for w in workspaces if w.output == output.name])) - - # Windows Title - #if container: - # t.append(container.name) - - # CPU - t.append('C' + ''.join([upChart(p/100) for p in psutil.cpu_percent(percpu=True)])) - - # Memory - t.append('M' + str(round(psutil.virtual_memory().percent)) + '% ' + - 'S' + str(round(psutil.swap_memory().percent)) + '%') - - # Disks - d = [] - for disk in psutil.disk_partitions(): - e = '' - if disk.device.startswith('/dev/sd'): - e += 'S' + disk.device[-2:].upper() - elif disk.device.startswith('/dev/mmcblk'): - e += 'M' + disk.device[-3] + disk.device[-1] - else: - e += '?' - e += ' ' - e += str(round(psutil.disk_usage(disk.mountpoint).percent)) + '%' - d.append(e) - t.append(' '.join(d)) - - # Network - netStats = psutil.net_if_stats() - netIO = psutil.net_io_counters(pernic=True) - net = [] - for iface in filter(lambda i: i != 'lo' and netStats[i].isup, netStats.keys()): - s = '' - if iface.startswith('eth'): - s += 'E' - elif iface.startswith('wlan'): - s += 'W' - else: - s += '?' - - s += ' ' - now = time() - global oldNetIO, oldTime - - sent = ((oldNetIO[iface].bytes_sent if iface in oldNetIO else 0) - (netIO[iface].bytes_sent if iface in netIO else 0)) / (oldTime - now) - recv = ((oldNetIO[iface].bytes_recv if iface in oldNetIO else 0) - (netIO[iface].bytes_recv if iface in netIO else 0)) / (oldTime - now) - s += '↓' + humanSizeOf(abs(recv), 'B/s') + ' ↑' + humanSizeOf(abs(sent), 'B/s') - - oldNetIO = netIO - oldTime = now - - net.append(s) - t.append(' '.join(net)) - - # Battery - if os.path.isdir('/sys/class/power_supply/BAT0'): - with open('/sys/class/power_supply/BAT0/charge_now') as f: - charge_now = int(f.read()) - with open('/sys/class/power_supply/BAT0/charge_full_design') as f: - charge_full = int(f.read()) - t.append('B' + str(round(100*charge_now/charge_full)) + '%') - - # Volume - # t.append('V ' + str(alsaaudio.Mixer('Master').getvolume()[0]) + '%') - - t.append(username + '@' + hostname) - - # print(' - '.join(t)) - # t = [output.name] - - z += ' - '.join(t) + '%{S' + str(aOutput + 1) + '}' - #lemonbar.stdin.write(bytes(' - '.join(t), 'utf-8')) - #lemonbar.stdin.write(bytes('%{S' + str(aOutput + 1) + '}', 'utf-8')) - - lemonbar.stdin.write(bytes(z+'\n', 'utf-8')) - lemonbar.stdin.flush() - -# Event listeners -def on_mode(i3, e): - global mode - if (e.change == 'default'): - mode = '' - else : - mode = e.change - update() - -i3.on("mode", on_mode) - -#def on_window_focus(i3, e): -# global container -# container = e.container -# update() -# -#i3.on("window::focus", on_window_focus) - -def on_workspace_focus(i3, e): - global workspaces - workspaces = i3.get_workspaces() - update() - -i3.on("workspace::focus", on_workspace_focus) - -# Starting - -update() - - -i3.main() + # TODO CPU provider + # TODO RAM provider + # TODO Temperature provider + # TODO Disk space provider + # TODO Screen (connected, autorandr configuration, bbswitch) provider + # TODO Unlocked keys provider + # TODO Mail provider + Bar.addSectionAll(TodoProvider(dir='~/.vdirsyncer/currentCalendars/'), BarGroupType.RIGHT) + Bar.addSectionAll(NetworkProvider(), BarGroupType.RIGHT) + Bar.addSectionAll(PulseaudioProvider(), BarGroupType.RIGHT) + Bar.addSectionAll(BatteryProvider(), BarGroupType.RIGHT) + Bar.addSectionAll(TimeProvider(), BarGroupType.RIGHT) diff --git a/config/lemonbar/new.py b/config/lemonbar/display.py similarity index 77% rename from config/lemonbar/new.py rename to config/lemonbar/display.py index 9b44e67..6aa68a8 100755 --- a/config/lemonbar/new.py +++ b/config/lemonbar/display.py @@ -1,23 +1,24 @@ #!/usr/bin/env python3 import enum -import logging import threading import time import subprocess -# TODO Update strategies (periodic, inotify file) -# TODO Section order (priority system maybe ?) # 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 +# but easier) # TODO Optimize to use write() calls instead of string concatenation (writing # BarGroup strings should be a good compromise) # TODO Use bytes rather than strings # TODO Use default colors of lemonbar sometimes +# TODO Mouse actions class BarGroupType(enum.Enum): LEFT = 0 RIGHT = 1 + # TODO Middle # MID_LEFT = 2 # MID_RIGHT = 3 @@ -28,24 +29,31 @@ class Bar: """ # Constants - FONT = "DejaVu Sans Mono for Powerline" + FONTS = ["DejaVu Sans Mono for Powerline", "Font Awesome"] @staticmethod def init(): Section.init() - cmd = ['lemonbar', '-f', Bar.FONT, '-b'] + cmd = ['lemonbar', '-b'] + for font in Bar.FONTS: + cmd += ["-f", font] Bar.process = subprocess.Popen(cmd, stdin=subprocess.PIPE) # Debug - # Bar(0) - Bar(1) + Bar(0) + # Bar(1) # Class globals everyone = set() string = "" process = None + @staticmethod + def forever(): + while True: + time.sleep(60) + def __init__(self, screen): assert isinstance(screen, int) self.screen = "%{S" + str(screen) + "}" @@ -94,7 +102,7 @@ class Bar: # Color for empty sections Bar.string += BarGroup.color(*Section.EMPTY) - print(Bar.string) + # print(Bar.string) Bar.process.stdin.write(bytes(Bar.string + '\n', 'utf-8')) Bar.process.stdin.flush() @@ -127,7 +135,12 @@ class BarGroup: def addSection(self, section): self.sections.append(section) - section.parents.add(self) + section.addParent(self) + + def addSectionAfter(self, sectionRef, section): + index = self.sections.index(sectionRef) + self.sections.insert(index + 1, section) + section.addParent(self) ALIGNS = {BarGroupType.LEFT: "%{l}", BarGroupType.RIGHT: "%{r}"} @@ -155,7 +168,7 @@ class BarGroup: if self.groupType == BarGroupType.LEFT: oSec = secs[s + 1] if s < lenS - 1 else None else: - oSec = secs[s - 1] if s > 1 else None + oSec = secs[s - 1] if s > 0 else None oTheme = Section.THEMES[oSec.theme] \ if oSec is not None else Section.EMPTY @@ -164,10 +177,16 @@ class BarGroup: parts.append(BarGroup.bgColor(theme[1])) parts.append(BarGroup.fgColor(theme[0])) parts.append(sec) - parts.append(BarGroup.color(theme[1], oTheme[1]) + "") + if theme == oTheme: + parts.append("") + else: + parts.append(BarGroup.color(theme[1], oTheme[1]) + "") else: - parts.append(BarGroup.fgColor(theme[1]) + "") - parts.append(BarGroup.color(*theme)) + if theme is oTheme: + parts.append("") + else: + parts.append(BarGroup.fgColor(theme[1]) + "") + parts.append(BarGroup.color(*theme)) parts.append(sec) @@ -189,21 +208,29 @@ class BarGroup: @staticmethod def updateAll(): - for group in BarGroup.everyone: - group.update() - Bar.updateAll() class SectionThread(threading.Thread): + ANIMATION_START = 0.025 + ANIMATION_STOP = 0.001 + ANIMATION_EVOLUTION = 0.9 def run(self): while Section.somethingChanged.wait(): Section.updateAll() + animTime = self.ANIMATION_START + frameTime = time.perf_counter() while len(Section.sizeChanging) > 0: - time.sleep(0.1) + frameTime += animTime + curTime = time.perf_counter() + sleepTime = frameTime - curTime + time.sleep(sleepTime if sleepTime > 0 else 0) Section.updateAll() + animTime *= self.ANIMATION_EVOLUTION + if animTime < self.ANIMATION_STOP: + animTime = self.ANIMATION_STOP class Section: @@ -218,19 +245,18 @@ class Section: THEMES = list() EMPTY = (FGCOLOR, BGCOLOR) + #: Sections that do not have their destination size + sizeChanging = set() + updateThread = SectionThread(daemon=True) + somethingChanged = threading.Event() + @staticmethod def init(): for t in range(8, 16): Section.THEMES.append((Section.COLORS[0], Section.COLORS[t])) - Section.updateThread = SectionThread(daemon=True) Section.updateThread.start() - #: Sections that do not have their destination size - sizeChanging = set() - somethingChanged = threading.Event() - updateThread = None - def __init__(self, theme=0): #: Displayed section #: Note: A section can be empty and displayed! @@ -252,10 +278,21 @@ class Section: self.parents = set() def __str__(self): - return "<{}><{}>{:01d}{}{:02d}/{:02d}" \ - .format(self.curText, self.dstText, - self.theme, "+" if self.visible else "-", - self.curSize, self.dstSize) + try: + return "<{}><{}>{:01d}{}{:02d}/{:02d}" \ + .format(self.curText, self.dstText, + self.theme, "+" if self.visible else "-", + self.curSize, self.dstSize) + except: + return super().__str__() + + def addParent(self, parent): + self.parents.add(parent) + + def appendAfter(self, section): + assert len(self.parents) + for parent in self.parents: + parent.addSectionAfter(self, section) def informParentsThemeChanged(self): for parent in self.parents: @@ -267,8 +304,15 @@ class Section: def updateText(self, text): if len(text): - self.dstText = ' {} '.format(text) - self.dstSize = len(self.dstText) + if isinstance(text, str): + text = [text] + text = [' '] + text + [' '] + + raw = [(t if isinstance(t, str) else t['text']) for t in text] + self.dstSize = sum([len(t) for t in raw]) + # TODO FEAT Handle colors + # TODO OPTI Not like that + self.dstText = ''.join(raw) else: self.dstSize = 0 @@ -331,6 +375,9 @@ class Section: Section.somethingChanged.clear() + @staticmethod + def ramp(p, ramp=" ▁▂▃▄▅▆▇█"): + return ramp[round(p * (len(ramp)-1))] if __name__ == '__main__': Bar.init() diff --git a/config/lemonbar/net.py b/config/lemonbar/net.py new file mode 100755 index 0000000..0254f9c --- /dev/null +++ b/config/lemonbar/net.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +import psutil +import subprocess + +netStats = psutil.net_if_stats() +netAddrs = psutil.net_if_addrs() +netIO = psutil.net_io_counters(pernic=True) + +for iface in netStats.keys(): + if not netStats[iface].isup or iface.startswith('lo'): + continue + + ssid = '' + if iface.startswith('eth') or iface.startswith('enp'): + icon = 'E' + elif iface.startswith('wlan') or iface.startswith('wlp'): + icon = 'W' + cmd = ["iwgetid", iface, "--raw"] + p = subprocess.run(cmd, stdout=subprocess.PIPE) + ssid = p.stdout.strip().decode() + # TODO Real icons + # TODO USB tethering + # TODO tan / tun + else: + icon = '?' + + print(icon, iface, ssid) + print(netIO[iface]) diff --git a/config/lemonbar/oldbar.py b/config/lemonbar/oldbar.py new file mode 100755 index 0000000..2b36ad5 --- /dev/null +++ b/config/lemonbar/oldbar.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 + +""" +Debugging script +""" + +import i3ipc +import os +import psutil +# import alsaaudio +from time import time +import subprocess + +i3 = i3ipc.Connection() +lemonbar = subprocess.Popen(['lemonbar', '-b'], stdin=subprocess.PIPE) + +# Utils +def upChart(p): + block = ' ▁▂▃▄▅▆▇█' + return block[round(p * (len(block)-1))] + +def humanSizeOf(num, suffix='B'): # TODO Credit + for unit in ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']: + if abs(num) < 1024.0: + return "%3.0f%2s%s" % (num, unit, suffix) + num /= 1024.0 + return "%.0f%2s%s" % (num, 'Yi', suffix) + +# Values +mode = '' +container = i3.get_tree().find_focused() +workspaces = i3.get_workspaces() +outputs = i3.get_outputs() + +username = os.environ['USER'] +hostname = os.environ['HOSTNAME'] +if '-' in hostname: + hostname = hostname.split('-')[-1] + +oldNetIO = dict() +oldTime = time() + +def update(): + activeOutputs = sorted(sorted(list(filter(lambda o: o.active, outputs)), key=lambda o: o.rect.y), key=lambda o: o.rect.x) + z = '' + for aOutput in range(len(activeOutputs)): + output = activeOutputs[aOutput] + # Mode || Workspaces + t = [] + if (mode != ''): + t.append(mode) + else: + t.append(' '.join([(w.name.upper() if w.focused else w.name) for w in workspaces if w.output == output.name])) + + # Windows Title + #if container: + # t.append(container.name) + + # CPU + t.append('C' + ''.join([upChart(p/100) for p in psutil.cpu_percent(percpu=True)])) + + # Memory + t.append('M' + str(round(psutil.virtual_memory().percent)) + '% ' + + 'S' + str(round(psutil.swap_memory().percent)) + '%') + + # Disks + d = [] + for disk in psutil.disk_partitions(): + e = '' + if disk.device.startswith('/dev/sd'): + e += 'S' + disk.device[-2:].upper() + elif disk.device.startswith('/dev/mmcblk'): + e += 'M' + disk.device[-3] + disk.device[-1] + else: + e += '?' + e += ' ' + e += str(round(psutil.disk_usage(disk.mountpoint).percent)) + '%' + d.append(e) + t.append(' '.join(d)) + + # Network + netStats = psutil.net_if_stats() + netIO = psutil.net_io_counters(pernic=True) + net = [] + for iface in filter(lambda i: i != 'lo' and netStats[i].isup, netStats.keys()): + s = '' + if iface.startswith('eth'): + s += 'E' + elif iface.startswith('wlan'): + s += 'W' + else: + s += '?' + + s += ' ' + now = time() + global oldNetIO, oldTime + + sent = ((oldNetIO[iface].bytes_sent if iface in oldNetIO else 0) - (netIO[iface].bytes_sent if iface in netIO else 0)) / (oldTime - now) + recv = ((oldNetIO[iface].bytes_recv if iface in oldNetIO else 0) - (netIO[iface].bytes_recv if iface in netIO else 0)) / (oldTime - now) + s += '↓' + humanSizeOf(abs(recv), 'B/s') + ' ↑' + humanSizeOf(abs(sent), 'B/s') + + oldNetIO = netIO + oldTime = now + + net.append(s) + t.append(' '.join(net)) + + # Battery + if os.path.isdir('/sys/class/power_supply/BAT0'): + with open('/sys/class/power_supply/BAT0/charge_now') as f: + charge_now = int(f.read()) + with open('/sys/class/power_supply/BAT0/charge_full_design') as f: + charge_full = int(f.read()) + t.append('B' + str(round(100*charge_now/charge_full)) + '%') + + # Volume + # t.append('V ' + str(alsaaudio.Mixer('Master').getvolume()[0]) + '%') + + t.append(username + '@' + hostname) + + # print(' - '.join(t)) + # t = [output.name] + + z += ' - '.join(t) + '%{S' + str(aOutput + 1) + '}' + #lemonbar.stdin.write(bytes(' - '.join(t), 'utf-8')) + #lemonbar.stdin.write(bytes('%{S' + str(aOutput + 1) + '}', 'utf-8')) + + lemonbar.stdin.write(bytes(z+'\n', 'utf-8')) + lemonbar.stdin.flush() + +# Event listeners +def on_mode(i3, e): + global mode + if (e.change == 'default'): + mode = '' + else : + mode = e.change + update() + +i3.on("mode", on_mode) + +#def on_window_focus(i3, e): +# global container +# container = e.container +# update() +# +#i3.on("window::focus", on_window_focus) + +def on_workspace_focus(i3, e): + global workspaces + workspaces = i3.get_workspaces() + update() + +i3.on("workspace::focus", on_workspace_focus) + +# Starting + +update() + + +i3.main() diff --git a/config/lemonbar/providers.py b/config/lemonbar/providers.py new file mode 100755 index 0000000..1e435ee --- /dev/null +++ b/config/lemonbar/providers.py @@ -0,0 +1,397 @@ +#!/usr/bin/env python3 + +import datetime +from updaters import * +from display import * +import pulsectl +import psutil +import subprocess +import socket +import ipaddress +import logging +import coloredlogs +import json +import i3ipc +from pprint import pprint + +coloredlogs.install(level='DEBUG', fmt='%(levelname)s %(message)s') +log = logging.getLogger() + + +def humanSize(num): + """ + Returns a string of width 3+3 + """ + 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.0 + return "{:d}YiB".format(num) + + +class TimeProvider(Section, PeriodicUpdater): + def fetcher(self): + now = datetime.datetime.now() + return now.strftime('%d/%m/%y %H:%M:%S') + + def __init__(self): + PeriodicUpdater.__init__(self) + Section.__init__(self) + self.changeInterval(1) + + +class BatteryProvider(Section, PeriodicUpdater): + + RAMP = "" + + def fetcher(self): + with open(self.batdir + 'status') as f: + status = f.read().strip() + if status == "Full": + return "" + elif status == "Discharging": + icon = "" + elif status == "Charging": + icon = "" + else: + log.warn("Unknwon battery status: {}".format(status)) + icon = "?" + with open(self.batdir + 'capacity') as f: + capacity = int(f.read()) + icon += self.ramp(capacity/100, self.RAMP) + return '{} {}%'.format(icon, capacity) + + def __init__(self, battery='BAT0'): + PeriodicUpdater.__init__(self) + Section.__init__(self) + + self.batdir = '/sys/class/power_supply/{}/'.format(battery) + assert os.path.isdir(self.batdir) + + self.changeInterval(5) + + +class PulseaudioProvider(Section, ThreadedUpdater): + def __init__(self): + ThreadedUpdater.__init__(self) + Section.__init__(self) + self.pulseEvents = pulsectl.Pulse('event-handler') + + self.pulseEvents.event_mask_set(pulsectl.PulseEventMaskEnum.sink) + self.pulseEvents.event_callback_set(self.handleEvent) + self.start() + self.refreshData() + + + def fetcher(self): + sinks = [] + with pulsectl.Pulse('list-sinks') as pulse: + for sink in pulse.sink_list(): + vol = pulse.volume_get_all_chans(sink) + if vol > 1: + vol = 1 + + if sink.port_active.name == "analog-output-headphones": + icon = "" + elif sink.port_active.name == "analog-output-speaker": + icon = "" + else: + icon = "?" + + ramp = "" if sink.mute else (" " + self.ramp(vol)) + sinks.append(icon + ramp) + return " ".join(sinks) + + def loop(self): + self.pulseEvents.event_listen() + + def handleEvent(self, ev): + self.refreshData() + +class NetworkProviderSection(Section, Updater): + THEME = 5 + + 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 + + # Get addresses + ipv4 = None + ipv6 = None + for address in self.parent.addrs[self.iface]: + if address.family == socket.AF_INET: + ipv4 = address + elif address.family == socket.AF_INET6: + ipv6 = address + if ipv4 is None and ipv6 is None: + return text + + # Set icon + if self.iface.startswith('eth') or self.iface.startswith('enp'): + if 'u' in self.iface: + text = '' + else: + text = '' + elif self.iface.startswith('wlan') or self.iface.startswith('wlp'): + text = '' + cmd = ["iwgetid", self.iface, "--raw"] + p = subprocess.run(cmd, stdout=subprocess.PIPE) + + text += ' ' + p.stdout.strip().decode() + elif self.iface.startswith('tun') or self.iface.startswith('tap'): + text = '' + else: + text = '?' + + if self.showAddress: + if ipv4: + netStrFull = '{}/{}'.format(ipv4.address, ipv4.netmask) + addr = ipaddress.IPv4Network(netStrFull, strict=False) + text += ' {}/{}'.format(ipv4.address, addr.prefixlen) + # TODO IPV6 + # if ipv6: + # text += ' ' + ipv6.address + + if self.showSpeed: + recvDiff = self.parent.IO[self.iface].bytes_recv \ + - self.parent.prevIO[self.iface].bytes_recv + sentDiff = self.parent.IO[self.iface].bytes_sent \ + - self.parent.prevIO[self.iface].bytes_sent + recvDiff /= self.parent.dt + sentDiff /= self.parent.dt + text += ' ↓{}↑{}'.format(humanSize(recvDiff), humanSize(sentDiff)) + + if self.showTransfer: + text += ' ⇓{}⇑{}'.format( + humanSize(self.parent.IO[self.iface].bytes_recv), + humanSize(self.parent.IO[self.iface].bytes_sent)) + + 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 + self.showAddress = state >= 1 + self.showSpeed = state >= 2 + self.showTransfer = state >= 3 + + def __init__(self, iface, parent): + Section.__init__(self, theme=self.THEME) + self.iface = iface + self.parent = parent + self.changeState(1) + self.refreshData() + + +class NetworkProvider(Section, PeriodicUpdater): + def fetchData(self): + self.prev = self.last + self.prevIO = self.IO + + self.stats = psutil.net_if_stats() + self.addrs = psutil.net_if_addrs() + self.IO = psutil.net_io_counters(pernic=True) + self.ifaces = self.stats.keys() + + self.last = time.perf_counter() + self.dt = self.last - self.prev + + def refreshData(self): + self.fetchData() + + lastSection = self + for iface in sorted(list(self.ifaces)): + if iface not in self.sections.keys(): + section = NetworkProviderSection(iface, self) + lastSection.appendAfter(section) + self.sections[iface] = section + else: + section = self.sections[iface] + section.refreshData() + lastSection = section + + def addParent(self, parent): + self.parents.add(parent) + self.refreshData() + + def __init__(self): + PeriodicUpdater.__init__(self) + Section.__init__(self) + + self.sections = dict() + self.last = 0 + self.IO = dict() + self.fetchData() + + +class TodoProvider(Section, InotifyUpdater): + # TODO OPT/UX Maybe we could get more data from the todoman python module + # TODO OPT Specific callback for specific directory + + def updateCalendarList(self): + calendars = sorted(os.listdir(self.dir)) + for calendar in calendars: + # If the calendar wasn't in the list + if calendar not in self.calendars: + self.addPath(os.path.join(self.dir, calendar), refresh=False) + self.calendars = calendars + + def __init__(self, dir): + """ + :parm str dir: [main]path value in todoman.conf + """ + InotifyUpdater.__init__(self) + Section.__init__(self) + self.dir = os.path.realpath(os.path.expanduser(dir)) + assert os.path.isdir(self.dir) + + self.calendars = [] + self.addPath(self.dir) + + def getName(self, calendar): + path = os.path.join(self.dir, calendar, 'displayname') + with open(path, 'r') as f: + name = f.read().strip() + return name + + def getColor(self, calendar): + path = os.path.join(self.dir, calendar, 'color') + with open(path, 'r') as f: + color = f.read().strip() + return color + + def countUndone(self, calendar): + cmd = ["todo", "--porcelain", "list", self.getName(calendar)] + proc = subprocess.run(cmd, stdout=subprocess.PIPE) + data = json.loads(proc.stdout) + return len(data) + + def fetcher(self): + text = [] + self.updateCalendarList() + for calendar in self.calendars: + c = self.countUndone(calendar) + if c > 0: + color = self.getColor(calendar) + text += [' '] + text += [{"text": str(c), "fgColor": color}] + if len(text): + return [''] + text + else: + return '' + +class I3Provider(Section, ThreadedUpdater): + # TODO Multi-screen + + THEME_NORMAL = 0 + THEME_FOCUSED = 2 + THEME_URGENT = 1 + THEME_MODE = 1 + + def setName(self, section, origName): + # TODO Custom names + if origName: + section.fullName = origName + else: + section.fullName = '' + section.updateText(section.fullName) + + def initialPopulation(self, parent): + """ + Called on init + Can't reuse addWorkspace since i3.get_workspaces() gives dict and not + ConObjects + """ + workspaces = self.i3.get_workspaces() + lastSection = self.modeSection + for workspace in workspaces: + # if parent.display != workspace["display"]: + # continue + theme = self.THEME_FOCUSED if workspace["focused"] \ + else (self.THEME_URGENT if workspace["urgent"] + else self.THEME_NORMAL) + section = Section(theme=theme) + parent.addSectionAfter(lastSection, section) + self.setName(section, workspace["name"]) + self.sections[workspace["num"]] = section + lastSection = section + + def on_workspace(self, i3, e): + print(304, e.change) + + def on_workspace_init(self, i3, e): + workspace = e.current + i = workspace.num + if i in self.sections: + section = self.sections[i] + else: + while i not in self.sections.keys() and i > 0: + i -= 1 + prevSection = self.sections[i] if i != 0 else self.modeSection + section = Section() + self.sections[workspace.num] = section + prevSection.appendAfter(section) + self.setName(section, workspace.name) + + def on_workspace_empty(self, i3, e): + workspace = e.current + section = self.sections[workspace.num] + self.setName(section, None) + + def on_workspace_focus(self, i3, e): + self.sections[e.current.num].updateTheme(self.THEME_FOCUSED) + self.sections[e.old.num].updateTheme(self.THEME_NORMAL) + + def on_workspace_urgent(self, i3, e): + self.sections[e.current.num].updateTheme(self.THEME_URGENT) + + def on_workspace_rename(self, i3, e): + self.sections[e.current.num].updateText(e.name) + + def on_mode(self, i3, e): + if e.change == 'default': + self.modeSection.updateText('') + for section in self.sections.values(): + section.updateText(section.fullName) + else: + self.modeSection.updateText(e.change) + for section in self.sections.values(): + section.updateText('') + + def __init__(self): + ThreadedUpdater.__init__(self) + Section.__init__(self) + + self.i3 = i3ipc.Connection() + self.sections = dict() + self.i3.on("workspace::init", self.on_workspace_init) + self.i3.on("workspace::focus", self.on_workspace_focus) + self.i3.on("workspace::empty", self.on_workspace_empty) + self.i3.on("workspace::urgent", self.on_workspace_urgent) + self.i3.on("workspace::rename", self.on_workspace_rename) + # TODO Un-handled/tested: reload, rename, restored, move + + self.i3.on("mode", self.on_mode) + + self.modeSection = Section(theme=self.THEME_MODE) + self.start() + + def addParent(self, parent): + self.parents.add(parent) + parent.addSection(self.modeSection) + self.initialPopulation(parent) + + def loop(self): + self.i3.main() diff --git a/config/lemonbar/updaters.py b/config/lemonbar/updaters.py new file mode 100755 index 0000000..f421371 --- /dev/null +++ b/config/lemonbar/updaters.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 + +import math +import functools +import threading +import pyinotify +import os +import time + +# TODO Multiple providers for the same section + + +class Updater: + @staticmethod + def init(): + PeriodicUpdater.init() + InotifyUpdater.init() + + def updateText(self, text): + print(text) + + def fetcher(self): + return "{} refreshed".format(self) + + def refreshData(self): + data = self.fetcher() + self.updateText(data) + + +class PeriodicUpdaterThread(threading.Thread): + def run(self): + counter = 0 + while True: + if PeriodicUpdater.intervalsChanged \ + .wait(timeout=PeriodicUpdater.intervalStep): + # ↑ sleeps here + PeriodicUpdater.intervalsChanged.clear() + counter = 0 + for providerList in PeriodicUpdater.intervals.copy().values(): + for provider in providerList.copy(): + provider.refreshData() + else: + counter += PeriodicUpdater.intervalStep + counter = counter % PeriodicUpdater.intervalLoop + for interval in PeriodicUpdater.intervals.keys(): + if counter % interval == 0: + for provider in PeriodicUpdater.intervals[interval]: + provider.refreshData() + + +class PeriodicUpdater(Updater): + """ + Needs to call :func:`PeriodicUpdater.changeInterval` in `__init__` + """ + + intervals = dict() + intervalStep = None + intervalLoop = None + updateThread = PeriodicUpdaterThread(daemon=True) + intervalsChanged = threading.Event() + + @staticmethod + def gcds(*args): + return functools.reduce(math.gcd, args) + + @staticmethod + def lcm(a, b): + """Return lowest common multiple.""" + return a * b // math.gcd(a, b) + + @staticmethod + def lcms(*args): + """Return lowest common multiple.""" + return functools.reduce(PeriodicUpdater.lcm, args) + + @staticmethod + def updateIntervals(): + intervalsList = list(PeriodicUpdater.intervals.keys()) + PeriodicUpdater.intervalStep = PeriodicUpdater.gcds(*intervalsList) + PeriodicUpdater.intervalLoop = PeriodicUpdater.lcms(*intervalsList) + PeriodicUpdater.intervalsChanged.set() + + @staticmethod + def init(): + PeriodicUpdater.updateThread.start() + + def __init__(self): + Updater.__init__(self) + self.interval = None + + def changeInterval(self, interval): + assert isinstance(interval, int) + + if self.interval is not None: + PeriodicUpdater.intervals[self.interval].remove(self) + + self.interval = interval + + if interval not in PeriodicUpdater.intervals: + PeriodicUpdater.intervals[interval] = set() + PeriodicUpdater.intervals[interval].add(self) + + PeriodicUpdater.updateIntervals() + + +class InotifyUpdaterEventHandler(pyinotify.ProcessEvent): + + def process_default(self, event): + # DEBUG + # from pprint import pprint + # pprint(event.__dict__) + # return + + assert event.path in InotifyUpdater.paths + + if 0 in InotifyUpdater.paths[event.path]: + for provider in InotifyUpdater.paths[event.path][0]: + provider.refreshData() + + if event.name in InotifyUpdater.paths[event.path]: + for provider in InotifyUpdater.paths[event.path][event.name]: + provider.refreshData() + + +class InotifyUpdater(Updater): + """ + Needs to call :func:`PeriodicUpdater.changeInterval` in `__init__` + """ + + wm = pyinotify.WatchManager() + paths = dict() + + @staticmethod + def init(): + notifier = pyinotify.ThreadedNotifier(InotifyUpdater.wm, + InotifyUpdaterEventHandler()) + notifier.start() + + # TODO Mask for folders + MASK = pyinotify.IN_CREATE | pyinotify.IN_MODIFY | pyinotify.IN_DELETE + + def addPath(self, path, refresh=True): + path = os.path.realpath(os.path.expanduser(path)) + + # Detect if file or folder + if os.path.isdir(path): + self.dirpath = path + # 0: Directory watcher + self.filename = 0 + elif os.path.isfile(path): + self.dirpath = os.path.dirname(path) + self.filename = os.path.basename(path) + else: + raise FileNotFoundError("No such file or directory: '{}'" + .format(path)) + + # Register watch action + if self.dirpath not in InotifyUpdater.paths: + InotifyUpdater.paths[self.dirpath] = dict() + if self.filename not in InotifyUpdater.paths[self.dirpath]: + InotifyUpdater.paths[self.dirpath][self.filename] = set() + InotifyUpdater.paths[self.dirpath][self.filename].add(self) + + # Add watch + InotifyUpdater.wm.add_watch(self.dirpath, InotifyUpdater.MASK) + + if refresh: + self.refreshData() + + +class ThreadedUpdaterThread(threading.Thread): + def __init__(self, updater, *args, **kwargs): + self.updater = updater + threading.Thread.__init__(self, *args, **kwargs) + + def run(self): + while True: + self.updater.loop() + + +class ThreadedUpdater(Updater): + """ + Must implement loop(), and call start() + """ + + def __init__(self): + self.thread = ThreadedUpdaterThread(self, daemon=True) + + def loop(self): + self.refreshData() + time.sleep(10) + + def start(self): + self.thread.start() diff --git a/config/lemonbar/x.py b/config/lemonbar/x.py new file mode 100755 index 0000000..ff08c01 --- /dev/null +++ b/config/lemonbar/x.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 + +import Xlib.display + +dis = Xlib.display.Display() + +nb = dis.screen_count() + +for s in range(nb): + print(s) diff --git a/config/polybar/config b/config/polybar/config index a58ef5d..4ad73ba 100644 --- a/config/polybar/config +++ b/config/polybar/config @@ -76,7 +76,7 @@ enable-ipc = true inherit = bar/base modules-center = mpd -modules-right = mail todo vpncheck eth wlan bbswitch xbacklight volume battery shortdate +modules-right = cpu memory temperature mail todo vpncheck eth wlan bbswitch xbacklight volume battery shortdate tray-position = right tray-padding = 2 diff --git a/scripts/remcrlf b/scripts/remcrlf index ca0f98c..09e5882 100755 --- a/scripts/remcrlf +++ b/scripts/remcrlf @@ -2,4 +2,11 @@ # Removes CRLF (^M or \r) from a file -sed -e "s/^M//" $1 -i +#sed -e "s/^M//" "$1" -i + +tmpfile=$(mktemp) + +cp "$1" "$tmpfile" +tr -d '\r' < "$tmpfile" > "$1" +rm "$tmpfile" +