Bar actions
This commit is contained in:
parent
b39ce22885
commit
ae0c7c7c09
|
@ -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
|
||||
|
|
|
@ -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__':
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue