Bar actions
This commit is contained in:
parent
b39ce22885
commit
ae0c7c7c09
|
@ -3,11 +3,16 @@
|
||||||
from providers import *
|
from providers import *
|
||||||
|
|
||||||
# TODO If multiple screen, expand the sections and share them
|
# TODO If multiple screen, expand the sections and share them
|
||||||
|
# TODO Graceful exit
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
Bar.init()
|
Bar.init()
|
||||||
Updater.init()
|
Updater.init()
|
||||||
|
|
||||||
|
# Bar.addSectionAll(NotmuchUnreadProvider(dir='~/.mail/', theme=1), BarGroupType.RIGHT)
|
||||||
|
# Bar.addSectionAll(TimeProvider(theme=1), BarGroupType.RIGHT)
|
||||||
|
# else:
|
||||||
|
|
||||||
WORKSPACE_THEME = 0
|
WORKSPACE_THEME = 0
|
||||||
FOCUS_THEME = 3
|
FOCUS_THEME = 3
|
||||||
URGENT_THEME = 1
|
URGENT_THEME = 1
|
||||||
|
|
|
@ -4,6 +4,11 @@ import enum
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import subprocess
|
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
|
# 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
|
# 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
|
# 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:
|
class Bar:
|
||||||
"""
|
"""
|
||||||
One bar for each screen
|
One bar for each screen
|
||||||
|
@ -40,7 +58,10 @@ class Bar:
|
||||||
cmd = ['lemonbar', '-b']
|
cmd = ['lemonbar', '-b']
|
||||||
for font in Bar.FONTS:
|
for font in Bar.FONTS:
|
||||||
cmd += ["-f", "{}:size={}".format(font, Bar.FONTSIZE)]
|
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
|
# Debug
|
||||||
Bar(0)
|
Bar(0)
|
||||||
|
@ -51,6 +72,25 @@ class Bar:
|
||||||
string = ""
|
string = ""
|
||||||
process = None
|
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
|
@staticmethod
|
||||||
def forever():
|
def forever():
|
||||||
while True:
|
while True:
|
||||||
|
@ -288,6 +328,8 @@ class Section:
|
||||||
#: Groups that have this section
|
#: Groups that have this section
|
||||||
self.parents = set()
|
self.parents = set()
|
||||||
|
|
||||||
|
self.actions = set()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
try:
|
try:
|
||||||
return "<{}><{}>{:01d}{}{:02d}/{:02d}" \
|
return "<{}><{}>{:01d}{}{:02d}/{:02d}" \
|
||||||
|
@ -314,6 +356,8 @@ class Section:
|
||||||
parent.childsTextChanged = True
|
parent.childsTextChanged = True
|
||||||
|
|
||||||
def parseParts(self, parts, bit='', clo=''):
|
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):
|
if isinstance(parts, str):
|
||||||
parts = [parts]
|
parts = [parts]
|
||||||
|
|
||||||
|
@ -344,10 +388,30 @@ class Section:
|
||||||
if 'overline' in part:
|
if 'overline' in part:
|
||||||
newBit = newBit + '%{+o}'
|
newBit = newBit + '%{+o}'
|
||||||
newClo = '%{-o}' + newClo
|
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
|
bits += newBits
|
||||||
clos += newClos
|
clos += newClos
|
||||||
bit += newClo
|
|
||||||
else:
|
else:
|
||||||
raise RuntimeError()
|
raise RuntimeError()
|
||||||
|
|
||||||
|
@ -357,14 +421,14 @@ class Section:
|
||||||
# TODO FEAT Actions
|
# TODO FEAT Actions
|
||||||
# TODO OPTI When srcSize == dstSize, maybe the bit array isn't
|
# TODO OPTI When srcSize == dstSize, maybe the bit array isn't
|
||||||
# needed
|
# needed
|
||||||
|
oldActions = self.actions.copy()
|
||||||
|
|
||||||
if len(text):
|
if len(text):
|
||||||
if isinstance(text, str):
|
if isinstance(text, str):
|
||||||
# TODO OPTI This common case
|
# TODO OPTI This common case
|
||||||
text = [text]
|
text = [text]
|
||||||
|
|
||||||
self.dstBits, self.dstClos = self.parseParts([' '] + 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
|
# TODO FEAT Half-spaces
|
||||||
|
|
||||||
self.dstText = ''.join(self.dstBits)
|
self.dstText = ''.join(self.dstBits)
|
||||||
|
@ -372,6 +436,9 @@ class Section:
|
||||||
else:
|
else:
|
||||||
self.dstSize = 0
|
self.dstSize = 0
|
||||||
|
|
||||||
|
for action in oldActions:
|
||||||
|
self.unregsiterAction(action)
|
||||||
|
|
||||||
if self.curSize == self.dstSize:
|
if self.curSize == self.dstSize:
|
||||||
self.curText = self.dstText
|
self.curText = self.dstText
|
||||||
self.informParentsTextChanged()
|
self.informParentsTextChanged()
|
||||||
|
@ -444,9 +511,69 @@ class Section:
|
||||||
|
|
||||||
|
|
||||||
class StatefulSection(Section):
|
class StatefulSection(Section):
|
||||||
# TODO Next thing to do
|
|
||||||
# TODO Allow to temporary expand the section (e.g. when important change)
|
# 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__':
|
if __name__ == '__main__':
|
||||||
|
|
|
@ -37,16 +37,21 @@ def randomColor(seed=0):
|
||||||
return '#{:02x}{:02x}{:02x}'.format(*[random.randint(0, 255) for _ in range(3)])
|
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):
|
def fetcher(self):
|
||||||
now = datetime.datetime.now()
|
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):
|
def __init__(self, theme=None):
|
||||||
PeriodicUpdater.__init__(self)
|
PeriodicUpdater.__init__(self)
|
||||||
Section.__init__(self, theme)
|
StatefulSection.__init__(self, theme)
|
||||||
self.changeInterval(1)
|
self.changeInterval(1) # TODO OPTI When state < 1
|
||||||
|
|
||||||
|
|
||||||
class CpuProvider(Section, PeriodicUpdater):
|
class CpuProvider(Section, PeriodicUpdater):
|
||||||
def fetcher(self):
|
def fetcher(self):
|
||||||
|
@ -185,7 +190,9 @@ class PulseaudioProvider(Section, ThreadedUpdater):
|
||||||
self.refreshData()
|
self.refreshData()
|
||||||
|
|
||||||
|
|
||||||
class NetworkProviderSection(Section, Updater):
|
class NetworkProviderSection(StatefulSection, Updater):
|
||||||
|
|
||||||
|
NUMBER_STATES = 4
|
||||||
|
|
||||||
def getIcon(self):
|
def getIcon(self):
|
||||||
if self.iface.startswith('eth') or self.iface.startswith('enp'):
|
if self.iface.startswith('eth') or self.iface.startswith('enp'):
|
||||||
|
@ -194,11 +201,13 @@ class NetworkProviderSection(Section, Updater):
|
||||||
else:
|
else:
|
||||||
return ['']
|
return ['']
|
||||||
elif self.iface.startswith('wlan') or self.iface.startswith('wlp'):
|
elif self.iface.startswith('wlan') or self.iface.startswith('wlp'):
|
||||||
cmd = ["iwgetid", self.iface, "--raw"]
|
if self.state > 0:
|
||||||
p = subprocess.run(cmd, stdout=subprocess.PIPE)
|
cmd = ["iwgetid", self.iface, "--raw"]
|
||||||
|
p = subprocess.run(cmd, stdout=subprocess.PIPE)
|
||||||
ssid = p.stdout.strip().decode()
|
ssid = p.stdout.strip().decode()
|
||||||
return [' {}'.format(ssid)]
|
return [' {}'.format(ssid)]
|
||||||
|
else:
|
||||||
|
return ['']
|
||||||
elif self.iface.startswith('tun') or self.iface.startswith('tap'):
|
elif self.iface.startswith('tun') or self.iface.startswith('tap'):
|
||||||
return ['']
|
return ['']
|
||||||
else:
|
else:
|
||||||
|
@ -255,25 +264,16 @@ class NetworkProviderSection(Section, Updater):
|
||||||
|
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def cycleState(self):
|
def onChangeState(self, state):
|
||||||
newState = (self.state + 1) % 4
|
|
||||||
self.changeState(newState)
|
|
||||||
|
|
||||||
def changeState(self, state):
|
|
||||||
assert isinstance(state, int)
|
|
||||||
assert state < 4
|
|
||||||
self.state = state
|
|
||||||
self.showAddress = state >= 1
|
self.showAddress = state >= 1
|
||||||
self.showSpeed = state >= 2
|
self.showSpeed = state >= 2
|
||||||
self.showTransfer = state >= 3
|
self.showTransfer = state >= 3
|
||||||
|
|
||||||
def __init__(self, iface, parent):
|
def __init__(self, iface, parent):
|
||||||
Updater.__init__(self)
|
Updater.__init__(self)
|
||||||
Section.__init__(self, theme=parent.theme)
|
StatefulSection.__init__(self, theme=parent.theme)
|
||||||
self.iface = iface
|
self.iface = iface
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.changeState(1)
|
|
||||||
self.refreshData()
|
|
||||||
|
|
||||||
|
|
||||||
class NetworkProvider(Section, PeriodicUpdater):
|
class NetworkProvider(Section, PeriodicUpdater):
|
||||||
|
@ -372,30 +372,27 @@ class KeystoreProvider(Section, MergedUpdater):
|
||||||
MergedUpdater.__init__(self, SshAgentProvider(), GpgAgentProvider(), prefix=[' '])
|
MergedUpdater.__init__(self, SshAgentProvider(), GpgAgentProvider(), prefix=[' '])
|
||||||
Section.__init__(self, theme)
|
Section.__init__(self, theme)
|
||||||
|
|
||||||
|
class NotmuchUnreadProvider(ColorCountsSection, PeriodicUpdater):
|
||||||
class NotmuchUnreadProvider(Section, PeriodicUpdater):
|
|
||||||
# TODO OPTI Transform InotifyUpdater (watching notmuch folder should be
|
# TODO OPTI Transform InotifyUpdater (watching notmuch folder should be
|
||||||
# enough)
|
# enough)
|
||||||
def fetcher(self):
|
ICON = ''
|
||||||
|
|
||||||
|
def subfetcher(self):
|
||||||
db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY, path=self.dir)
|
db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY, path=self.dir)
|
||||||
text = []
|
counts = []
|
||||||
for account in self.accounts:
|
for account in self.accounts:
|
||||||
queryStr = 'folder:/{}/ and tag:unread'.format(account)
|
queryStr = 'folder:/{}/ and tag:unread'.format(account)
|
||||||
query = notmuch.Query(db, queryStr)
|
query = notmuch.Query(db, queryStr)
|
||||||
nbMsgs = query.count_messages()
|
nbMsgs = query.count_messages()
|
||||||
if nbMsgs < 1:
|
if nbMsgs < 1:
|
||||||
continue
|
continue
|
||||||
text += [' ']
|
counts.append((nbMsgs, self.colors[account]))
|
||||||
text += [{"cont": str(nbMsgs), "fgColor": self.colors[account]}]
|
|
||||||
db.close()
|
db.close()
|
||||||
if len(text):
|
return counts
|
||||||
return [''] + text
|
|
||||||
else:
|
|
||||||
return ''
|
|
||||||
|
|
||||||
def __init__(self, dir='~/.mail/', theme=None):
|
def __init__(self, dir='~/.mail/', theme=None):
|
||||||
PeriodicUpdater.__init__(self)
|
PeriodicUpdater.__init__(self)
|
||||||
Section.__init__(self, theme)
|
ColorCountsSection.__init__(self, theme)
|
||||||
|
|
||||||
self.dir = os.path.realpath(os.path.expanduser(dir))
|
self.dir = os.path.realpath(os.path.expanduser(dir))
|
||||||
assert os.path.isdir(self.dir)
|
assert os.path.isdir(self.dir)
|
||||||
|
@ -414,9 +411,10 @@ class NotmuchUnreadProvider(Section, PeriodicUpdater):
|
||||||
self.changeInterval(10)
|
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/UX Maybe we could get more data from the todoman python module
|
||||||
# TODO OPT Specific callback for specific directory
|
# TODO OPT Specific callback for specific directory
|
||||||
|
ICON = ''
|
||||||
|
|
||||||
def updateCalendarList(self):
|
def updateCalendarList(self):
|
||||||
calendars = sorted(os.listdir(self.dir))
|
calendars = sorted(os.listdir(self.dir))
|
||||||
|
@ -431,7 +429,7 @@ class TodoProvider(Section, InotifyUpdater):
|
||||||
:parm str dir: [main]path value in todoman.conf
|
:parm str dir: [main]path value in todoman.conf
|
||||||
"""
|
"""
|
||||||
InotifyUpdater.__init__(self)
|
InotifyUpdater.__init__(self)
|
||||||
Section.__init__(self, theme)
|
ColorCountsSection.__init__(self, theme=theme)
|
||||||
self.dir = os.path.realpath(os.path.expanduser(dir))
|
self.dir = os.path.realpath(os.path.expanduser(dir))
|
||||||
assert os.path.isdir(self.dir)
|
assert os.path.isdir(self.dir)
|
||||||
|
|
||||||
|
@ -456,19 +454,15 @@ class TodoProvider(Section, InotifyUpdater):
|
||||||
data = json.loads(proc.stdout)
|
data = json.loads(proc.stdout)
|
||||||
return len(data)
|
return len(data)
|
||||||
|
|
||||||
def fetcher(self):
|
def subfetcher(self):
|
||||||
text = []
|
counts = []
|
||||||
self.updateCalendarList()
|
self.updateCalendarList()
|
||||||
for calendar in self.calendars:
|
for calendar in self.calendars:
|
||||||
c = self.countUndone(calendar)
|
c = self.countUndone(calendar)
|
||||||
if c > 0:
|
if c <= 0:
|
||||||
color = self.getColor(calendar)
|
continue
|
||||||
text += [' ']
|
counts.append((c, self.getColor(calendar)))
|
||||||
text += [{"cont": str(c), "fgColor": color}]
|
return counts
|
||||||
if len(text):
|
|
||||||
return [''] + text
|
|
||||||
else:
|
|
||||||
return ''
|
|
||||||
|
|
||||||
class I3WindowTitleProvider(Section, I3Updater):
|
class I3WindowTitleProvider(Section, I3Updater):
|
||||||
# TODO FEAT To make this available from start, we need to find the
|
# 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:
|
except BaseException as e:
|
||||||
log.error(e, exc_info=True)
|
log.error(e, exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue