Bar refactor

Nothing visible except more and less bugs, but it's waaaaay easier to
work with the code :)
This commit is contained in:
Geoffrey Frogeye 2018-09-06 17:00:46 +02:00
parent ae0c7c7c09
commit ca9d5e279e
4 changed files with 260 additions and 215 deletions

View file

@ -9,10 +9,6 @@ if __name__ == "__main__":
Bar.init()
Updater.init()
# Bar.addSectionAll(NotmuchUnreadProvider(dir='~/.mail/', theme=1), BarGroupType.RIGHT)
# Bar.addSectionAll(TimeProvider(theme=1), BarGroupType.RIGHT)
# else:
WORKSPACE_THEME = 0
FOCUS_THEME = 3
URGENT_THEME = 1
@ -28,10 +24,10 @@ if __name__ == "__main__":
SYSTEM_THEME = 2
DANGER_THEME = FOCUS_THEME
CRITICAL_THEME = URGENT_THEME
Bar.addSectionAll(CpuProvider(theme=SYSTEM_THEME, themeDanger=DANGER_THEME, themeCritical=CRITICAL_THEME), BarGroupType.RIGHT)
Bar.addSectionAll(RamProvider(theme=SYSTEM_THEME, themeDanger=DANGER_THEME, themeCritical=CRITICAL_THEME), BarGroupType.RIGHT)
Bar.addSectionAll(TemperatureProvider(theme=SYSTEM_THEME, themeDanger=DANGER_THEME, themeCritical=CRITICAL_THEME), BarGroupType.RIGHT)
Bar.addSectionAll(BatteryProvider(theme=SYSTEM_THEME, themeDanger=DANGER_THEME, themeCritical=CRITICAL_THEME), BarGroupType.RIGHT)
Bar.addSectionAll(CpuProvider(), BarGroupType.RIGHT)
Bar.addSectionAll(RamProvider(), BarGroupType.RIGHT)
Bar.addSectionAll(TemperatureProvider(), BarGroupType.RIGHT)
Bar.addSectionAll(BatteryProvider(), BarGroupType.RIGHT)
# Peripherals
PERIPHERAL_THEME = 5

View file

@ -17,7 +17,6 @@ log = logging.getLogger()
# BarGroup strings should be a good compromise)
# TODO Use bytes rather than strings
# TODO Use default colors of lemonbar sometimes
# TODO Mouse actions
# TODO Adapt bar height with font height
@ -144,7 +143,6 @@ class Bar:
# 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()
@ -194,10 +192,6 @@ class BarGroup:
def bgColor(color):
return "%{B" + (color or '-') + "}"
@staticmethod
def underlineColor(color):
return "%{U" + (color or '-') + "}"
@staticmethod
def color(fg, bg):
return BarGroup.fgColor(fg) + BarGroup.bgColor(bg)
@ -291,6 +285,9 @@ class Section:
THEMES = list()
EMPTY = (FGCOLOR, BGCOLOR)
ICON = None
PERSISTENT = False
#: Sections that do not have their destination size
sizeChanging = set()
updateThread = SectionThread(daemon=True)
@ -321,14 +318,15 @@ class Section:
self.curSize = 0
#: Destination text
self.dstText = ''
self.dstText = Text(' ', Text(), ' ')
#: Destination size
self.dstSize = 0
#: Groups that have this section
self.parents = set()
self.actions = set()
self.icon = self.ICON
def __str__(self):
try:
@ -355,92 +353,24 @@ class Section:
for parent in self.parents:
parent.childsTextChanged = True
def parseParts(self, parts, bit='', clo=''):
# TODO OPTI Shall benefit of a major refactor, using Objects rather
# than dicts and a stack-based approac
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
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
bits += newBits
clos += newClos
else:
raise RuntimeError()
return bits, clos
def updateText(self, text):
# TODO FEAT Actions
# TODO OPTI When srcSize == dstSize, maybe the bit array isn't
# needed
oldActions = self.actions.copy()
if len(text):
assert not isinstance(text, list) # Old behaviour, keep it until everything is cleaned
if isinstance(text, str):
# TODO OPTI This common case
text = [text]
text = Text(text)
self.dstBits, self.dstClos = self.parseParts([' '] + text + [' '])
# TODO FEAT Half-spaces
if self.icon:
self.dstText[0] = ' ' + self.icon + ' '
self.dstText = ''.join(self.dstBits)
self.dstSize = len(self.dstBits)
if text is None:
self.dstSize = 3 if self.PERSISTENT else 0
else:
self.dstSize = 0
for action in oldActions:
self.unregsiterAction(action)
self.dstText[1] = text
self.dstText.setSection(self)
self.dstSize = len(self.dstText)
if self.curSize == self.dstSize:
self.curText = self.dstText
if self.dstSize > 0:
self.curText = str(self.dstText)
self.informParentsTextChanged()
else:
Section.sizeChanging.add(self)
@ -449,6 +379,8 @@ class Section:
def updateTheme(self, theme):
assert isinstance(theme, int)
assert theme < len(Section.THEMES)
if theme == self.theme:
return
self.theme = theme
self.informParentsThemeChanged()
Section.somethingChanged.set()
@ -482,9 +414,7 @@ class Section:
Section.sizeChanging.remove(self)
return
closPos = self.curSize-1
clos = self.dstClos[closPos] if closPos < len(self.dstClos) else ''
self.curText = ''.join(Section.fit(self.dstBits, self.curSize)) + clos
self.curText = self.dstText.text(size=self.curSize, pad=True)
self.informParentsTextChanged()
@staticmethod
@ -512,13 +442,15 @@ class Section:
class StatefulSection(Section):
# TODO Allow to temporary expand the section (e.g. when important change)
NUMBER_STATES = 4
NUMBER_STATES = None
def __init__(self, *args, **kwargs):
Section.__init__(self, *args, **kwargs)
self.state = 0
if hasattr(self, 'onChangeState'):
self.onChangeState(self.state)
self.dstText.setDecorators(scrollUp=self.incrementState,
scrollDown=self.decrementState)
def incrementState(self):
newState = min(self.state + 1, self.NUMBER_STATES - 1)
@ -536,18 +468,9 @@ class StatefulSection(Section):
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 = '?'
COLORABLE_ICON = '?'
def __init__(self, theme=None):
StatefulSection.__init__(self, theme=theme)
@ -556,59 +479,154 @@ class ColorCountsSection(StatefulSection):
counts = self.subfetcher()
# Nothing
if not len(counts):
return ''
return None
# Icon colored
elif self.state == 0 and len(counts) == 1:
count, color = counts[0]
return [{'fgColor': color, "cont": self.ICON}]
return Text(self.COLORABLE_ICON, fg=color)
# Icon
elif self.state == 0 and len(counts) > 1:
return self.ICON
return Text(self.COLORABLE_ICON)
# Icon + Total
elif self.state == 1 and len(counts) > 1:
total = sum([count for count, color in counts])
return [self.ICON, ' ', str(total)]
return Text(self.COLORABLE_ICON, ' ', total)
# Icon + Counts
else:
text = [self.ICON]
text = Text(self.COLORABLE_ICON)
for count, color in counts:
text += [' ', {'fgColor': color, "cont": str(count)}]
text.append(' ', Text(count, fg=color))
return text
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)
class Text:
def _setElements(self, elements):
# TODO OPTI Concatenate consecutrive string
self.elements = list(elements)
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)
def _setDecorators(self, decorators):
# TODO OPTI Convert no decorator to strings
self.decorators = decorators
self.prefix = None
self.suffix = None
def __init__(self, *args, **kwargs):
self._setElements(args)
self._setDecorators(kwargs)
self.section = None
def append(self, *args):
self._setElements(self.elements + list(args))
def prepend(self, *args):
self._setElements(list(args) + self.elements)
def setElements(self, *args):
self._setElements(args)
def setDecorators(self, **kwargs):
self._setDecorators(kwargs)
def setSection(self, section):
assert isinstance(section, Section)
self.section = section
for element in self.elements:
if isinstance(element, Text):
element.setSection(section)
def _genFixs(self):
if self.prefix is not None and self.suffix is not None:
return
self.prefix = ''
self.suffix = ''
def nest(prefix, suffix):
self.prefix = self.prefix + '%{' + prefix + '}'
self.suffix = '%{' + suffix + '}' + self.suffix
def getColor(val):
# TODO Allow themes
assert isinstance(val, str) and len(val) == 7
return val
def button(number, function):
handle = Bar.getFunctionHandle(function)
nest('A' + number + ':' + handle.decode() + ':', 'A' + number)
for key, val in self.decorators.items():
if key == 'fg':
reset = self.section.THEMES[self.section.theme][0]
nest('F' + getColor(val), 'F' + reset)
elif key == 'bg':
reset = self.section.THEMES[self.section.theme][1]
nest('B' + getColor(val), 'B' + reset)
elif key == "clickLeft":
button('1', val)
elif key == "clickMiddle":
button('2', val)
elif key == "clickRight":
button('3', val)
elif key == "scrollUp":
button('4', val)
elif key == "scrollDown":
button('5', val)
else:
log.warn("Unkown decorator: {}".format(key))
def _text(self, size=None, pad=False):
self._genFixs()
curString = self.prefix
curSize = 0
remSize = size
for element in self.elements:
if element is None:
continue
elif isinstance(element, Text):
newString, newSize = element._text(size=remSize)
else:
newString = str(element)
if remSize is not None:
newString = newString[:remSize]
newSize = len(newString)
curString += newString
curSize += newSize
if remSize is not None:
remSize -= newSize
if remSize <= 0:
break
curString += self.suffix
if pad and remSize > 0:
curString += ' ' * remSize
curSize += remSize
if size is not None:
if pad:
assert size == curSize
else:
assert size >= curSize
return curString, curSize
def text(self, *args, **kwargs):
string, size = self._text(*args, **kwargs)
return string
def __str__(self):
# TODO OPTI
return self.text()
def __len__(self):
# TODO OPTI
string, size = self._text()
return size
def __getitem__(self, index):
return self.elements[index]
def __setitem__(self, index, data):
self.elements[index] = data

View file

@ -53,25 +53,60 @@ class TimeProvider(StatefulSection, PeriodicUpdater):
StatefulSection.__init__(self, theme)
self.changeInterval(1) # TODO OPTI When state < 1
class CpuProvider(Section, PeriodicUpdater):
class AlertLevel(enum.Enum):
NORMAL = 0
WARNING = 1
DANGER = 2
class AlertingSection(StatefulSection):
# TODO EASE Correct settings for themes
THEMES = {AlertLevel.NORMAL: 2,
AlertLevel.WARNING: 3,
AlertLevel.DANGER: 1}
PERSISTENT = True
def getLevel(self, quantity):
if quantity > self.dangerThresold:
return AlertLevel.DANGER
elif quantity > self.warningThresold:
return AlertLevel.WARNING
else:
return AlertLevel.NORMAL
def updateLevel(self, quantity):
self.level = self.getLevel(quantity)
self.updateTheme(self.THEMES[self.level])
if self.level == AlertLevel.NORMAL:
return
# TODO Temporary update state
def __init__(self, theme):
StatefulSection.__init__(self, theme)
self.dangerThresold = 0.90
self.warningThresold = 0.75
class CpuProvider(AlertingSection, PeriodicUpdater):
NUMBER_STATES = 3
ICON = ''
def fetcher(self):
percents = psutil.cpu_percent(percpu=True)
percent = psutil.cpu_percent(percpu=False)
theme = self.themeCritical if percent >= 90 else \
(self.themeDanger if percent >= 75 else self.themeNormal)
self.updateTheme(theme)
return '' + ''.join([Section.ramp(p/100) for p in percents])
self.updateLevel(percent/100)
if self.state >= 2:
percents = psutil.cpu_percent(percpu=True)
return ''.join([Section.ramp(p/100) for p in percents])
elif self.state >= 1:
return Section.ramp(percent/100)
def __init__(self, theme=None, themeDanger=None, themeCritical=None):
AlertingSection.__init__(self, theme)
PeriodicUpdater.__init__(self)
Section.__init__(self, theme)
self.themeNormal = theme or self.theme
self.themeDanger = themeDanger or self.theme
self.themeCritical = themeCritical or self.theme
self.changeInterval(1)
class RamProvider(Section, PeriodicUpdater):
# TODO Use AlertingSection
"""
Shows free RAM
"""
@ -93,7 +128,7 @@ class RamProvider(Section, PeriodicUpdater):
self.changeInterval(1)
class TemperatureProvider(Section, PeriodicUpdater):
# TODO FEAT Change color when > high > critical
# TODO Use AlertingSection
RAMP = ""
@ -124,7 +159,7 @@ class TemperatureProvider(Section, PeriodicUpdater):
class BatteryProvider(Section, PeriodicUpdater):
# TODO Change color when < thresold%
# TODO Use AlertingSection
RAMP = ""
@ -194,24 +229,23 @@ class NetworkProviderSection(StatefulSection, Updater):
NUMBER_STATES = 4
def getIcon(self):
def actType(self):
self.ssid = None
if self.iface.startswith('eth') or self.iface.startswith('enp'):
if 'u' in self.iface:
return ['']
self.icon = ''
else:
return ['']
self.icon = ''
elif self.iface.startswith('wlan') or self.iface.startswith('wlp'):
if self.state > 0:
self.icon = ''
if self.showSsid:
cmd = ["iwgetid", self.iface, "--raw"]
p = subprocess.run(cmd, stdout=subprocess.PIPE)
ssid = p.stdout.strip().decode()
return ['{}'.format(ssid)]
else:
return ['']
self.ssid = p.stdout.strip().decode()
elif self.iface.startswith('tun') or self.iface.startswith('tap'):
return ['']
self.icon = ''
else:
return ['?']
self.icon = '?'
def getAddresses(self):
ipv4 = None
@ -224,26 +258,28 @@ class NetworkProviderSection(StatefulSection, Updater):
return ipv4, ipv6
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
return None
# Get addresses
ipv4, ipv6 = self.getAddresses()
if ipv4 is None and ipv6 is None:
return text
return None
text = self.getIcon()
text = []
self.actType()
if self.showSsid and self.ssid:
text.append(self.ssid)
if self.showAddress:
if ipv4:
netStrFull = '{}/{}'.format(ipv4.address, ipv4.netmask)
addr = ipaddress.IPv4Network(netStrFull, strict=False)
addrStr = '{}/{}'.format(ipv4.address, addr.prefixlen)
text += [addrStr]
text.append(addrStr)
# TODO IPV6
# if ipv6:
# text += ' ' + ipv6.address
@ -255,16 +291,18 @@ class NetworkProviderSection(StatefulSection, Updater):
- self.parent.prevIO[self.iface].bytes_sent
recvDiff /= self.parent.dt
sentDiff /= self.parent.dt
text += ['{}{}'.format(humanSize(recvDiff), humanSize(sentDiff))]
text.append('{}{}'.format(humanSize(recvDiff),
humanSize(sentDiff)))
if self.showTransfer:
text += [' {}{}'.format(
text.append('{}{}'.format(
humanSize(self.parent.IO[self.iface].bytes_recv),
humanSize(self.parent.IO[self.iface].bytes_sent))]
humanSize(self.parent.IO[self.iface].bytes_sent)))
return text
return ' '.join(text)
def onChangeState(self, state):
self.showSsid = state >= 0
self.showAddress = state >= 1
self.showSpeed = state >= 2
self.showTransfer = state >= 3
@ -307,7 +345,7 @@ class NetworkProvider(Section, PeriodicUpdater):
for section in self.sections.values():
section.refreshData()
return ''
return None
def addParent(self, parent):
self.parents.add(parent)
@ -328,15 +366,13 @@ class SshAgentProvider(PeriodicUpdater):
cmd = ["ssh-add", "-l"]
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
if proc.returncode != 0:
return ''
text = []
return None
text = Text()
for line in proc.stdout.split(b'\n'):
if not len(line):
continue
fingerprint = line.split()[1]
text += [{"cont": '',
"fgColor": randomColor(seed=fingerprint)
}]
text.append(Text('', fg=randomColor(seed=fingerprint)))
return text
def __init__(self):
@ -349,8 +385,8 @@ class GpgAgentProvider(PeriodicUpdater):
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
# proc = subprocess.run(cmd)
if proc.returncode != 0:
return ''
text = []
return None
text = Text()
for line in proc.stdout.split(b'\n'):
if not len(line) or line == b'OK':
continue
@ -358,9 +394,7 @@ class GpgAgentProvider(PeriodicUpdater):
if spli[6] != b'1':
continue
keygrip = spli[2]
text += [{"cont": '',
"fgColor": randomColor(seed=keygrip)
}]
text.append(Text('', fg=randomColor(seed=keygrip)))
return text
def __init__(self):
@ -368,14 +402,16 @@ class GpgAgentProvider(PeriodicUpdater):
self.changeInterval(5)
class KeystoreProvider(Section, MergedUpdater):
ICON = ''
def __init__(self, theme=None):
MergedUpdater.__init__(self, SshAgentProvider(), GpgAgentProvider(), prefix=[''])
MergedUpdater.__init__(self, SshAgentProvider(), GpgAgentProvider())
Section.__init__(self, theme)
class NotmuchUnreadProvider(ColorCountsSection, PeriodicUpdater):
# TODO OPTI Transform InotifyUpdater (watching notmuch folder should be
# enough)
ICON = ''
COLORABLE_ICON = ''
def subfetcher(self):
db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY, path=self.dir)
@ -414,7 +450,7 @@ class NotmuchUnreadProvider(ColorCountsSection, PeriodicUpdater):
class TodoProvider(ColorCountsSection, InotifyUpdater):
# TODO OPT/UX Maybe we could get more data from the todoman python module
# TODO OPT Specific callback for specific directory
ICON = ''
COLORABLE_ICON = ''
def updateCalendarList(self):
calendars = sorted(os.listdir(self.dir))
@ -575,6 +611,7 @@ class I3WorkspacesProvider(Section, I3Updater):
self.initialPopulation(parent)
class MpdProvider(Section, ThreadedUpdater):
# TODO FEAT More informations and controls
MAX_LENGTH = 50
@ -610,9 +647,7 @@ class MpdProvider(Section, ThreadedUpdater):
if len(infosStr) > MpdProvider.MAX_LENGTH:
infosStr = infosStr[:MpdProvider.MAX_LENGTH-1] + ''
text = ["{}".format(infosStr)]
return text
return "{}".format(infosStr)
def loop(self):
try:
@ -624,3 +659,4 @@ class MpdProvider(Section, ThreadedUpdater):
except BaseException as e:
log.error(e, exc_info=True)

View file

@ -9,6 +9,7 @@ import time
import logging
import coloredlogs
import i3ipc
from display import Text
coloredlogs.install(level='DEBUG', fmt='%(levelname)s %(message)s')
log = logging.getLogger()
@ -231,23 +232,17 @@ class MergedUpdater(Updater):
# TODO OPTI Do not update until end of periodic batch
def fetcher(self):
text = []
text = Text()
for updater in self.updaters:
data = self.texts[updater]
if not len(data):
continue
if isinstance(data, str):
data = [data]
text += data
if not len(text):
return ''
return self.prefix + text + self.suffix
text.append(self.texts[updater])
# TODO OPTI After Text.__len__
# if not len(text):
# return None
return text
def __init__(self, *args, prefix=[''], suffix=['']):
def __init__(self, *args):
Updater.__init__(self)
self.prefix = prefix
self.suffix = suffix
self.updaters = []
self.texts = dict()