dotfiles/config/lemonbar/display.py

418 lines
12 KiB
Python
Raw Normal View History

2018-08-21 19:50:44 +00:00
#!/usr/bin/env python3
import enum
import threading
import time
2018-08-22 10:04:55 +00:00
import subprocess
2018-08-21 19:50:44 +00:00
# TODO Allow deletion of Bar, BarGroup and Section for screen changes
2018-09-05 07:07:37 +00:00
# IDEA Use i3 ipc events rather than relying on xrandr or Xlib (less portable
# but easier)
2018-08-21 19:50:44 +00:00
# TODO Optimize to use write() calls instead of string concatenation (writing
# BarGroup strings should be a good compromise)
2018-08-22 10:04:55 +00:00
# TODO Use bytes rather than strings
# TODO Use default colors of lemonbar sometimes
2018-09-05 07:07:37 +00:00
# TODO Mouse actions
2018-08-21 19:50:44 +00:00
class BarGroupType(enum.Enum):
LEFT = 0
RIGHT = 1
2018-09-05 07:07:37 +00:00
# TODO Middle
2018-08-21 19:50:44 +00:00
# MID_LEFT = 2
# MID_RIGHT = 3
class Bar:
"""
One bar for each screen
"""
2018-08-22 10:04:55 +00:00
# Constants
2018-09-05 07:07:37 +00:00
FONTS = ["DejaVu Sans Mono for Powerline", "Font Awesome"]
2018-08-21 19:50:44 +00:00
@staticmethod
def init():
Section.init()
2018-09-05 07:07:37 +00:00
cmd = ['lemonbar', '-b']
for font in Bar.FONTS:
cmd += ["-f", font]
2018-08-22 10:04:55 +00:00
Bar.process = subprocess.Popen(cmd, stdin=subprocess.PIPE)
2018-08-21 19:50:44 +00:00
# Debug
2018-09-05 07:07:37 +00:00
Bar(0)
# Bar(1)
2018-08-22 10:04:55 +00:00
# Class globals
everyone = set()
string = ""
process = None
2018-08-21 19:50:44 +00:00
2018-09-05 07:07:37 +00:00
@staticmethod
def forever():
while True:
time.sleep(60)
2018-08-22 10:04:55 +00:00
def __init__(self, screen):
assert isinstance(screen, int)
self.screen = "%{S" + str(screen) + "}"
2018-08-21 19:50:44 +00:00
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:
2018-08-22 10:04:55 +00:00
self.string = self.screen
self.string += self.groups[BarGroupType.LEFT].string
self.string += self.groups[BarGroupType.RIGHT].string
2018-08-21 19:50:44 +00:00
self.childsChanged = False
@staticmethod
def updateAll():
2018-08-22 10:04:55 +00:00
Bar.string = ""
2018-08-21 19:50:44 +00:00
for bar in Bar.everyone:
bar.update()
2018-08-22 10:04:55 +00:00
Bar.string += bar.string
# Color for empty sections
Bar.string += BarGroup.color(*Section.EMPTY)
2018-08-21 19:50:44 +00:00
2018-09-05 07:07:37 +00:00
# print(Bar.string)
2018-08-22 10:04:55 +00:00
Bar.process.stdin.write(bytes(Bar.string + '\n', 'utf-8'))
Bar.process.stdin.flush()
2018-08-21 19:50:44 +00:00
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 = ''
2018-08-22 10:04:55 +00:00
self.parts = []
2018-08-21 19:50:44 +00:00
#: 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)
2018-09-05 07:07:37 +00:00
section.addParent(self)
def addSectionAfter(self, sectionRef, section):
index = self.sections.index(sectionRef)
self.sections.insert(index + 1, section)
section.addParent(self)
2018-08-21 19:50:44 +00:00
2018-08-22 10:04:55 +00:00
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)
2018-08-21 19:50:44 +00:00
def update(self):
if self.childsThemeChanged:
2018-08-22 10:04:55 +00:00
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:
2018-09-05 07:07:37 +00:00
oSec = secs[s - 1] if s > 0 else None
2018-08-22 10:04:55 +00:00
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)
2018-09-05 07:07:37 +00:00
if theme == oTheme:
parts.append("")
else:
parts.append(BarGroup.color(theme[1], oTheme[1]) + "")
2018-08-22 10:04:55 +00:00
else:
2018-09-05 07:07:37 +00:00
if theme is oTheme:
parts.append("")
else:
parts.append(BarGroup.fgColor(theme[1]) + "")
parts.append(BarGroup.color(*theme))
2018-08-22 10:04:55 +00:00
parts.append(sec)
# TODO Concatenate successive strings
self.parts = parts
2018-08-21 19:50:44 +00:00
if self.childsTextChanged or self.childsThemeChanged:
2018-08-22 10:04:55 +00:00
self.string = ""
for part in self.parts:
if isinstance(part, str):
self.string += part
elif isinstance(part, Section):
self.string += part.curText
2018-08-21 19:50:44 +00:00
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):
2018-09-05 07:07:37 +00:00
ANIMATION_START = 0.025
ANIMATION_STOP = 0.001
ANIMATION_EVOLUTION = 0.9
2018-08-21 19:50:44 +00:00
def run(self):
while Section.somethingChanged.wait():
Section.updateAll()
2018-09-05 07:07:37 +00:00
animTime = self.ANIMATION_START
frameTime = time.perf_counter()
2018-08-21 19:50:44 +00:00
while len(Section.sizeChanging) > 0:
2018-09-05 07:07:37 +00:00
frameTime += animTime
curTime = time.perf_counter()
sleepTime = frameTime - curTime
time.sleep(sleepTime if sleepTime > 0 else 0)
2018-08-21 19:50:44 +00:00
Section.updateAll()
2018-09-05 07:07:37 +00:00
animTime *= self.ANIMATION_EVOLUTION
if animTime < self.ANIMATION_STOP:
animTime = self.ANIMATION_STOP
2018-08-21 19:50:44 +00:00
class Section:
2018-08-22 10:04:55 +00:00
# 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)
2018-08-21 19:50:44 +00:00
2018-09-05 07:07:37 +00:00
#: Sections that do not have their destination size
sizeChanging = set()
updateThread = SectionThread(daemon=True)
somethingChanged = threading.Event()
2018-08-21 19:50:44 +00:00
@staticmethod
def init():
2018-08-22 10:04:55 +00:00
for t in range(8, 16):
Section.THEMES.append((Section.COLORS[0], Section.COLORS[t]))
2018-08-21 19:50:44 +00:00
Section.updateThread.start()
2018-08-22 10:04:55 +00:00
def __init__(self, theme=0):
2018-08-21 19:50:44 +00:00
#: Displayed section
#: Note: A section can be empty and displayed!
self.visible = False
2018-08-22 10:04:55 +00:00
self.theme = theme
2018-08-21 19:50:44 +00:00
#: 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()
2018-08-22 10:04:55 +00:00
def __str__(self):
2018-09-05 07:07:37 +00:00
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)
2018-08-22 10:04:55 +00:00
2018-08-21 19:50:44 +00:00
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):
2018-09-05 07:07:37 +00:00
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)
2018-08-21 19:50:44 +00:00
else:
self.dstSize = 0
if self.curSize == self.dstSize:
self.curText = self.dstText
self.informParentsTextChanged()
else:
Section.sizeChanging.add(self)
Section.somethingChanged.set()
2018-08-22 10:04:55 +00:00
def updateTheme(self, theme):
assert isinstance(theme, int)
assert theme < len(Section.THEMES)
self.theme = theme
self.informParentsThemeChanged()
Section.somethingChanged.set()
2018-08-21 19:50:44 +00:00
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()
2018-09-05 07:07:37 +00:00
@staticmethod
def ramp(p, ramp=" ▁▂▃▄▅▆▇█"):
return ramp[round(p * (len(ramp)-1))]
2018-08-21 19:50:44 +00:00
if __name__ == '__main__':
Bar.init()
2018-08-22 10:04:55 +00:00
sec = Section(0)
sech = Section(1)
sec1 = Section(2)
sec2 = Section(3)
sec2h = Section(4)
sec3 = Section(5)
2018-08-21 19:50:44 +00:00
Bar.addSectionAll(sec, BarGroupType.LEFT)
2018-08-22 10:04:55 +00:00
Bar.addSectionAll(sech, BarGroupType.LEFT)
2018-08-21 19:50:44 +00:00
Bar.addSectionAll(sec1, BarGroupType.LEFT)
2018-08-22 10:04:55 +00:00
Bar.addSectionAll(sec2, BarGroupType.RIGHT)
Bar.addSectionAll(sec2h, BarGroupType.RIGHT)
Bar.addSectionAll(sec3, BarGroupType.RIGHT)
2018-08-21 19:50:44 +00:00
2018-08-22 10:04:55 +00:00
time.sleep(1)
sec.updateText("A")
time.sleep(1)
sec.updateText("")
time.sleep(1)
2018-08-21 19:50:44 +00:00
sec.updateText("Hello")
2018-08-22 10:04:55 +00:00
sec1.updateText("world!")
sec2.updateText("Salut")
sec2h.updateText("le")
sec3.updateText("monde !")
time.sleep(3)
sech.updateText("the")
sec2h.updateText("")
2018-08-21 19:50:44 +00:00
time.sleep(2)
sec.updateText("")
2018-08-22 10:04:55 +00:00
sech.updateText("")
sec1.updateText("")
sec2.updateText("")
sec2h.updateText("")
sec3.updateText("")
time.sleep(5)