dotfiles/config/lemonbar/display.py
2018-09-05 09:07:37 +02:00

418 lines
12 KiB
Python
Executable file

#!/usr/bin/env python3
import enum
import threading
import time
import subprocess
# 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
class Bar:
"""
One bar for each screen
"""
# Constants
FONTS = ["DejaVu Sans Mono for Powerline", "Font Awesome"]
@staticmethod
def init():
Section.init()
cmd = ['lemonbar', '-b']
for font in Bar.FONTS:
cmd += ["-f", font]
Bar.process = subprocess.Popen(cmd, stdin=subprocess.PIPE)
# Debug
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) + "}"
self.groups = dict()
for groupType in BarGroupType:
group = BarGroup(groupType, self)
self.groups[groupType] = group
self.childsChanged = False
self.everyone.add(self)
@staticmethod
def addSectionAll(section, group, screens=None):
"""
.. note::
Add the section before updating it for the first time.
"""
assert isinstance(section, Section)
assert isinstance(group, BarGroupType)
# TODO screens selection
for bar in Bar.everyone:
bar.addSection(section, group=group)
def addSection(self, section, group):
assert isinstance(section, Section)
assert isinstance(group, BarGroupType)
self.groups[group].addSection(section)
def update(self):
if self.childsChanged:
self.string = self.screen
self.string += self.groups[BarGroupType.LEFT].string
self.string += self.groups[BarGroupType.RIGHT].string
self.childsChanged = False
@staticmethod
def updateAll():
Bar.string = ""
for bar in Bar.everyone:
bar.update()
Bar.string += bar.string
# 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()
class BarGroup:
"""
One for each group of each bar
"""
everyone = set()
def __init__(self, groupType, parent):
assert isinstance(groupType, BarGroupType)
assert isinstance(parent, Bar)
self.groupType = groupType
self.parent = parent
self.sections = list()
self.string = ''
self.parts = []
#: One of the sections that had their theme or visibility changed
self.childsThemeChanged = False
#: One of the sections that had their text (maybe their size) changed
self.childsTextChanged = False
BarGroup.everyone.add(self)
def addSection(self, section):
self.sections.append(section)
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}"}
@staticmethod
def fgColor(color):
return "%{F" + color + "}"
@staticmethod
def bgColor(color):
return "%{B" + color + "}"
@staticmethod
def color(fg, bg):
return BarGroup.fgColor(fg) + BarGroup.bgColor(bg)
def update(self):
if self.childsThemeChanged:
parts = [BarGroup.ALIGNS[self.groupType]]
secs = [sec for sec in self.sections if sec.visible]
lenS = len(secs)
for s in range(lenS):
sec = secs[s]
theme = Section.THEMES[sec.theme]
if self.groupType == BarGroupType.LEFT:
oSec = secs[s + 1] if s < lenS - 1 else None
else:
oSec = secs[s - 1] if s > 0 else None
oTheme = Section.THEMES[oSec.theme] \
if oSec is not None else Section.EMPTY
if self.groupType == BarGroupType.LEFT:
if s == 0:
parts.append(BarGroup.bgColor(theme[1]))
parts.append(BarGroup.fgColor(theme[0]))
parts.append(sec)
if theme == oTheme:
parts.append("")
else:
parts.append(BarGroup.color(theme[1], oTheme[1]) + "")
else:
if theme is oTheme:
parts.append("")
else:
parts.append(BarGroup.fgColor(theme[1]) + "")
parts.append(BarGroup.color(*theme))
parts.append(sec)
# TODO Concatenate successive strings
self.parts = parts
if self.childsTextChanged or self.childsThemeChanged:
self.string = ""
for part in self.parts:
if isinstance(part, str):
self.string += part
elif isinstance(part, Section):
self.string += part.curText
self.parent.childsChanged = True
self.childsThemeChanged = False
self.childsTextChanged = False
@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:
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:
# TODO Update all of that to base16
COLORS = ['#002b36', '#dc322f', '#859900', '#b58900', '#268bd2', '#6c71c4',
'#2aa198', '#93a1a1', '#657b83', '#dc322f', '#859900', '#b58900',
'#268bd2', '#6c71c4', '#2aa198', '#fdf6e3']
FGCOLOR = '#93a1a1'
BGCOLOR = '#002b36'
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.start()
def __init__(self, theme=0):
#: Displayed section
#: Note: A section can be empty and displayed!
self.visible = False
self.theme = theme
#: Displayed text
self.curText = ''
#: Displayed text size
self.curSize = 0
#: Destination text
self.dstText = ''
#: Destination size
self.dstSize = 0
#: Groups that have this section
self.parents = set()
def __str__(self):
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:
parent.childsThemeChanged = True
def informParentsTextChanged(self):
for parent in self.parents:
parent.childsTextChanged = True
def updateText(self, text):
if len(text):
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
if self.curSize == self.dstSize:
self.curText = self.dstText
self.informParentsTextChanged()
else:
Section.sizeChanging.add(self)
Section.somethingChanged.set()
def updateTheme(self, theme):
assert isinstance(theme, int)
assert theme < len(Section.THEMES)
self.theme = theme
self.informParentsThemeChanged()
Section.somethingChanged.set()
def updateVisibility(self, visibility):
assert isinstance(visibility, bool)
self.visible = visibility
self.informParentsThemeChanged()
Section.somethingChanged.set()
@staticmethod
def fit(text, size):
t = len(text)
return text[:size] if t >= size else text + " " * (size - t)
def update(self):
# TODO Might profit of a better logic
if not self.visible:
self.updateVisibility(True)
return
if self.dstSize > self.curSize:
self.curSize += 1
elif self.dstSize < self.curSize:
self.curSize -= 1
else:
# Visibility toggling must be done one step after curSize = 0
if self.dstSize == 0:
self.updateVisibility(False)
Section.sizeChanging.remove(self)
return
self.curText = Section.fit(self.dstText, self.curSize)
self.informParentsTextChanged()
@staticmethod
def updateAll():
"""
Process all sections for text size changes
"""
for sizeChanging in Section.sizeChanging.copy():
sizeChanging.update()
BarGroup.updateAll()
Section.somethingChanged.clear()
@staticmethod
def ramp(p, ramp=" ▁▂▃▄▅▆▇█"):
return ramp[round(p * (len(ramp)-1))]
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)
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)