Bar actions

This commit is contained in:
Geoffrey Frogeye 2018-09-06 12:17:03 +02:00
parent b39ce22885
commit ae0c7c7c09
3 changed files with 178 additions and 53 deletions

View file

@ -3,11 +3,16 @@
from providers import *
# TODO If multiple screen, expand the sections and share them
# TODO Graceful exit
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

View file

@ -4,6 +4,11 @@ import enum
import threading
import time
import subprocess
import logging
import coloredlogs
coloredlogs.install(level='DEBUG', fmt='%(levelname)s %(message)s')
log = logging.getLogger()
# 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
@ -24,6 +29,19 @@ class BarGroupType(enum.Enum):
# MID_RIGHT = 3
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()
class Bar:
"""
One bar for each screen
@ -40,7 +58,10 @@ class Bar:
cmd = ['lemonbar', '-b']
for font in Bar.FONTS:
cmd += ["-f", "{}:size={}".format(font, Bar.FONTSIZE)]
Bar.process = subprocess.Popen(cmd, stdin=subprocess.PIPE)
Bar.process = subprocess.Popen(cmd, stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
Bar.stdoutThread = BarStdoutThread()
Bar.stdoutThread.start()
# Debug
Bar(0)
@ -51,6 +72,25 @@ class Bar:
string = ""
process = None
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
@staticmethod
def forever():
while True:
@ -288,6 +328,8 @@ class Section:
#: Groups that have this section
self.parents = set()
self.actions = set()
def __str__(self):
try:
return "<{}><{}>{:01d}{}{:02d}/{:02d}" \
@ -314,6 +356,8 @@ class Section:
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]
@ -344,10 +388,30 @@ class Section:
if 'overline' in part:
newBit = newBit + '%{+o}'
newClo = '%{-o}' + newClo
newBits, newClos = self.parseParts(part["cont"], bit=newBit, clo=clo+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
bit += newClo
else:
raise RuntimeError()
@ -357,14 +421,14 @@ class Section:
# TODO FEAT Actions
# TODO OPTI When srcSize == dstSize, maybe the bit array isn't
# needed
oldActions = self.actions.copy()
if len(text):
if isinstance(text, str):
# TODO OPTI This common case
text = [text]
self.dstBits, self.dstClos = self.parseParts([' '] + text + [' '])
# TODO BUG Try this and fix some closings that aren't done
# self.dstBits, self.dstClos = self.parseParts(text)
# TODO FEAT Half-spaces
self.dstText = ''.join(self.dstBits)
@ -372,6 +436,9 @@ class Section:
else:
self.dstSize = 0
for action in oldActions:
self.unregsiterAction(action)
if self.curSize == self.dstSize:
self.curText = self.dstText
self.informParentsTextChanged()
@ -444,9 +511,69 @@ class Section:
class StatefulSection(Section):
# TODO Next thing to do
# TODO Allow to temporary expand the section (e.g. when important change)
pass
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
if __name__ == '__main__':

View file

@ -37,16 +37,21 @@ def randomColor(seed=0):
return '#{:02x}{:02x}{:02x}'.format(*[random.randint(0, 255) for _ in range(3)])
class TimeProvider(Section, PeriodicUpdater):
class TimeProvider(StatefulSection, PeriodicUpdater):
FORMATS = ["%H:%M",
"%d/%m %H:%M:%S",
"%a %d/%m/%y %H:%M:%S"]
NUMBER_STATES = len(FORMATS)
def fetcher(self):
now = datetime.datetime.now()
return now.strftime('%d/%m/%y %H:%M:%S')
return now.strftime(self.FORMATS[self.state])
def __init__(self, theme=None):
PeriodicUpdater.__init__(self)
Section.__init__(self, theme)
self.changeInterval(1)
StatefulSection.__init__(self, theme)
self.changeInterval(1) # TODO OPTI When state < 1
class CpuProvider(Section, PeriodicUpdater):
def fetcher(self):
@ -185,7 +190,9 @@ class PulseaudioProvider(Section, ThreadedUpdater):
self.refreshData()
class NetworkProviderSection(Section, Updater):
class NetworkProviderSection(StatefulSection, Updater):
NUMBER_STATES = 4
def getIcon(self):
if self.iface.startswith('eth') or self.iface.startswith('enp'):
@ -194,11 +201,13 @@ class NetworkProviderSection(Section, Updater):
else:
return ['']
elif self.iface.startswith('wlan') or self.iface.startswith('wlp'):
cmd = ["iwgetid", self.iface, "--raw"]
p = subprocess.run(cmd, stdout=subprocess.PIPE)
ssid = p.stdout.strip().decode()
return ['{}'.format(ssid)]
if self.state > 0:
cmd = ["iwgetid", self.iface, "--raw"]
p = subprocess.run(cmd, stdout=subprocess.PIPE)
ssid = p.stdout.strip().decode()
return ['{}'.format(ssid)]
else:
return ['']
elif self.iface.startswith('tun') or self.iface.startswith('tap'):
return ['']
else:
@ -255,25 +264,16 @@ class NetworkProviderSection(Section, Updater):
return text
def cycleState(self):
newState = (self.state + 1) % 4
self.changeState(newState)
def changeState(self, state):
assert isinstance(state, int)
assert state < 4
self.state = state
def onChangeState(self, state):
self.showAddress = state >= 1
self.showSpeed = state >= 2
self.showTransfer = state >= 3
def __init__(self, iface, parent):
Updater.__init__(self)
Section.__init__(self, theme=parent.theme)
StatefulSection.__init__(self, theme=parent.theme)
self.iface = iface
self.parent = parent
self.changeState(1)
self.refreshData()
class NetworkProvider(Section, PeriodicUpdater):
@ -372,30 +372,27 @@ class KeystoreProvider(Section, MergedUpdater):
MergedUpdater.__init__(self, SshAgentProvider(), GpgAgentProvider(), prefix=[''])
Section.__init__(self, theme)
class NotmuchUnreadProvider(Section, PeriodicUpdater):
class NotmuchUnreadProvider(ColorCountsSection, PeriodicUpdater):
# TODO OPTI Transform InotifyUpdater (watching notmuch folder should be
# enough)
def fetcher(self):
ICON = ''
def subfetcher(self):
db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY, path=self.dir)
text = []
counts = []
for account in self.accounts:
queryStr = 'folder:/{}/ and tag:unread'.format(account)
query = notmuch.Query(db, queryStr)
nbMsgs = query.count_messages()
if nbMsgs < 1:
continue
text += [' ']
text += [{"cont": str(nbMsgs), "fgColor": self.colors[account]}]
counts.append((nbMsgs, self.colors[account]))
db.close()
if len(text):
return [''] + text
else:
return ''
return counts
def __init__(self, dir='~/.mail/', theme=None):
PeriodicUpdater.__init__(self)
Section.__init__(self, theme)
ColorCountsSection.__init__(self, theme)
self.dir = os.path.realpath(os.path.expanduser(dir))
assert os.path.isdir(self.dir)
@ -414,9 +411,10 @@ class NotmuchUnreadProvider(Section, PeriodicUpdater):
self.changeInterval(10)
class TodoProvider(Section, InotifyUpdater):
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 = ''
def updateCalendarList(self):
calendars = sorted(os.listdir(self.dir))
@ -431,7 +429,7 @@ class TodoProvider(Section, InotifyUpdater):
:parm str dir: [main]path value in todoman.conf
"""
InotifyUpdater.__init__(self)
Section.__init__(self, theme)
ColorCountsSection.__init__(self, theme=theme)
self.dir = os.path.realpath(os.path.expanduser(dir))
assert os.path.isdir(self.dir)
@ -456,19 +454,15 @@ class TodoProvider(Section, InotifyUpdater):
data = json.loads(proc.stdout)
return len(data)
def fetcher(self):
text = []
def subfetcher(self):
counts = []
self.updateCalendarList()
for calendar in self.calendars:
c = self.countUndone(calendar)
if c > 0:
color = self.getColor(calendar)
text += [' ']
text += [{"cont": str(c), "fgColor": color}]
if len(text):
return [''] + text
else:
return ''
if c <= 0:
continue
counts.append((c, self.getColor(calendar)))
return counts
class I3WindowTitleProvider(Section, I3Updater):
# TODO FEAT To make this available from start, we need to find the
@ -630,4 +624,3 @@ class MpdProvider(Section, ThreadedUpdater):
except BaseException as e:
log.error(e, exc_info=True)