418 lines
12 KiB
Python
Executable file
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)
|