dotfiles/config/lemonbar/display.py

615 lines
19 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-09-06 10:17:03 +00:00
import logging
import coloredlogs
coloredlogs.install(level='DEBUG', fmt='%(levelname)s %(message)s')
log = logging.getLogger()
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-09-06 05:38:22 +00:00
# TODO Adapt bar height with font height
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
2018-09-06 10:17:03 +00:00
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()
2018-08-21 19:50:44 +00:00
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-09-06 05:38:22 +00:00
FONTSIZE = 10
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:
2018-09-06 05:38:22 +00:00
cmd += ["-f", "{}:size={}".format(font, Bar.FONTSIZE)]
2018-09-06 10:17:03 +00:00
Bar.process = subprocess.Popen(cmd, stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
Bar.stdoutThread = BarStdoutThread()
Bar.stdoutThread.start()
2018-08-22 10:04:55 +00:00
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-06 10:17:03 +00:00
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
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):
2018-09-06 05:38:22 +00:00
return "%{F" + (color or '-') + "}"
2018-08-22 10:04:55 +00:00
@staticmethod
def bgColor(color):
2018-09-06 05:38:22 +00:00
return "%{B" + (color or '-') + "}"
@staticmethod
def underlineColor(color):
return "%{U" + (color or '-') + "}"
2018-08-22 10:04:55 +00:00
@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)
2018-09-06 05:38:22 +00:00
# TODO OPTI Concatenate successive strings
2018-08-22 10:04:55 +00:00
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-09-06 05:38:22 +00:00
lastChosenTheme = 0
2018-09-05 07:07:37 +00:00
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-09-06 05:38:22 +00:00
def __init__(self, theme=None):
2018-08-21 19:50:44 +00:00
#: Displayed section
#: Note: A section can be empty and displayed!
self.visible = False
2018-09-06 05:38:22 +00:00
if theme is None:
theme = Section.lastChosenTheme
Section.lastChosenTheme = (Section.lastChosenTheme + 1) \
% len(Section.THEMES)
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-09-06 10:17:03 +00:00
self.actions = 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
2018-09-06 05:38:22 +00:00
def parseParts(self, parts, bit='', clo=''):
2018-09-06 10:17:03 +00:00
# TODO OPTI Shall benefit of a major refactor, using Objects rather
# than dicts and a stack-based approac
2018-09-06 05:38:22 +00:00
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
2018-09-06 10:17:03 +00:00
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
2018-09-06 05:38:22 +00:00
bits += newBits
clos += newClos
else:
raise RuntimeError()
return bits, clos
2018-08-21 19:50:44 +00:00
def updateText(self, text):
2018-09-06 05:38:22 +00:00
# TODO FEAT Actions
# TODO OPTI When srcSize == dstSize, maybe the bit array isn't
# needed
2018-09-06 10:17:03 +00:00
oldActions = self.actions.copy()
2018-08-21 19:50:44 +00:00
if len(text):
2018-09-05 07:07:37 +00:00
if isinstance(text, str):
2018-09-06 05:38:22 +00:00
# TODO OPTI This common case
2018-09-05 07:07:37 +00:00
text = [text]
2018-09-06 05:38:22 +00:00
self.dstBits, self.dstClos = self.parseParts([' '] + text + [' '])
# TODO FEAT Half-spaces
self.dstText = ''.join(self.dstBits)
self.dstSize = len(self.dstBits)
2018-08-21 19:50:44 +00:00
else:
self.dstSize = 0
2018-09-06 10:17:03 +00:00
for action in oldActions:
self.unregsiterAction(action)
2018-08-21 19:50:44 +00:00
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)
2018-09-06 05:38:22 +00:00
return text[:size] if t >= size else text + [" "] * (size - t)
2018-08-21 19:50:44 +00:00
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
2018-09-06 05:38:22 +00:00
closPos = self.curSize-1
clos = self.dstClos[closPos] if closPos < len(self.dstClos) else ''
self.curText = ''.join(Section.fit(self.dstBits, self.curSize)) + clos
2018-08-21 19:50:44 +00:00
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=" ▁▂▃▄▅▆▇█"):
2018-09-06 05:38:22 +00:00
if p > 1:
return ramp[-1]
elif p < 0:
return ramp[0]
else:
return ramp[round(p * (len(ramp)-1))]
class StatefulSection(Section):
# TODO Allow to temporary expand the section (e.g. when important change)
2018-09-06 10:17:03 +00:00
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
2018-09-06 05:38:22 +00:00
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)