Bar bar bar
This commit is contained in:
parent
1c7efc4a76
commit
7987cdcaae
|
@ -1,161 +1,22 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Debugging script
|
||||
"""
|
||||
from providers import *
|
||||
|
||||
import i3ipc
|
||||
import os
|
||||
import psutil
|
||||
# import alsaaudio
|
||||
from time import time
|
||||
import subprocess
|
||||
if __name__ == "__main__":
|
||||
Bar.init()
|
||||
Updater.init()
|
||||
|
||||
i3 = i3ipc.Connection()
|
||||
lemonbar = subprocess.Popen(['lemonbar', '-b'], stdin=subprocess.PIPE)
|
||||
Bar.addSectionAll(I3Provider(), BarGroupType.LEFT)
|
||||
|
||||
# Utils
|
||||
def upChart(p):
|
||||
block = ' ▁▂▃▄▅▆▇█'
|
||||
return block[round(p * (len(block)-1))]
|
||||
|
||||
def humanSizeOf(num, suffix='B'): # TODO Credit
|
||||
for unit in ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']:
|
||||
if abs(num) < 1024.0:
|
||||
return "%3.0f%2s%s" % (num, unit, suffix)
|
||||
num /= 1024.0
|
||||
return "%.0f%2s%s" % (num, 'Yi', suffix)
|
||||
|
||||
# Values
|
||||
mode = ''
|
||||
container = i3.get_tree().find_focused()
|
||||
workspaces = i3.get_workspaces()
|
||||
outputs = i3.get_outputs()
|
||||
|
||||
username = os.environ['USER']
|
||||
hostname = os.environ['HOSTNAME']
|
||||
if '-' in hostname:
|
||||
hostname = hostname.split('-')[-1]
|
||||
|
||||
oldNetIO = dict()
|
||||
oldTime = time()
|
||||
|
||||
def update():
|
||||
activeOutputs = sorted(sorted(list(filter(lambda o: o.active, outputs)), key=lambda o: o.rect.y), key=lambda o: o.rect.x)
|
||||
z = ''
|
||||
for aOutput in range(len(activeOutputs)):
|
||||
output = activeOutputs[aOutput]
|
||||
# Mode || Workspaces
|
||||
t = []
|
||||
if (mode != ''):
|
||||
t.append(mode)
|
||||
else:
|
||||
t.append(' '.join([(w.name.upper() if w.focused else w.name) for w in workspaces if w.output == output.name]))
|
||||
|
||||
# Windows Title
|
||||
#if container:
|
||||
# t.append(container.name)
|
||||
|
||||
# CPU
|
||||
t.append('C' + ''.join([upChart(p/100) for p in psutil.cpu_percent(percpu=True)]))
|
||||
|
||||
# Memory
|
||||
t.append('M' + str(round(psutil.virtual_memory().percent)) + '% ' +
|
||||
'S' + str(round(psutil.swap_memory().percent)) + '%')
|
||||
|
||||
# Disks
|
||||
d = []
|
||||
for disk in psutil.disk_partitions():
|
||||
e = ''
|
||||
if disk.device.startswith('/dev/sd'):
|
||||
e += 'S' + disk.device[-2:].upper()
|
||||
elif disk.device.startswith('/dev/mmcblk'):
|
||||
e += 'M' + disk.device[-3] + disk.device[-1]
|
||||
else:
|
||||
e += '?'
|
||||
e += ' '
|
||||
e += str(round(psutil.disk_usage(disk.mountpoint).percent)) + '%'
|
||||
d.append(e)
|
||||
t.append(' '.join(d))
|
||||
|
||||
# Network
|
||||
netStats = psutil.net_if_stats()
|
||||
netIO = psutil.net_io_counters(pernic=True)
|
||||
net = []
|
||||
for iface in filter(lambda i: i != 'lo' and netStats[i].isup, netStats.keys()):
|
||||
s = ''
|
||||
if iface.startswith('eth'):
|
||||
s += 'E'
|
||||
elif iface.startswith('wlan'):
|
||||
s += 'W'
|
||||
else:
|
||||
s += '?'
|
||||
|
||||
s += ' '
|
||||
now = time()
|
||||
global oldNetIO, oldTime
|
||||
|
||||
sent = ((oldNetIO[iface].bytes_sent if iface in oldNetIO else 0) - (netIO[iface].bytes_sent if iface in netIO else 0)) / (oldTime - now)
|
||||
recv = ((oldNetIO[iface].bytes_recv if iface in oldNetIO else 0) - (netIO[iface].bytes_recv if iface in netIO else 0)) / (oldTime - now)
|
||||
s += '↓' + humanSizeOf(abs(recv), 'B/s') + ' ↑' + humanSizeOf(abs(sent), 'B/s')
|
||||
|
||||
oldNetIO = netIO
|
||||
oldTime = now
|
||||
|
||||
net.append(s)
|
||||
t.append(' '.join(net))
|
||||
|
||||
# Battery
|
||||
if os.path.isdir('/sys/class/power_supply/BAT0'):
|
||||
with open('/sys/class/power_supply/BAT0/charge_now') as f:
|
||||
charge_now = int(f.read())
|
||||
with open('/sys/class/power_supply/BAT0/charge_full_design') as f:
|
||||
charge_full = int(f.read())
|
||||
t.append('B' + str(round(100*charge_now/charge_full)) + '%')
|
||||
|
||||
# Volume
|
||||
# t.append('V ' + str(alsaaudio.Mixer('Master').getvolume()[0]) + '%')
|
||||
|
||||
t.append(username + '@' + hostname)
|
||||
|
||||
# print(' - '.join(t))
|
||||
# t = [output.name]
|
||||
|
||||
z += ' - '.join(t) + '%{S' + str(aOutput + 1) + '}'
|
||||
#lemonbar.stdin.write(bytes(' - '.join(t), 'utf-8'))
|
||||
#lemonbar.stdin.write(bytes('%{S' + str(aOutput + 1) + '}', 'utf-8'))
|
||||
|
||||
lemonbar.stdin.write(bytes(z+'\n', 'utf-8'))
|
||||
lemonbar.stdin.flush()
|
||||
|
||||
# Event listeners
|
||||
def on_mode(i3, e):
|
||||
global mode
|
||||
if (e.change == 'default'):
|
||||
mode = ''
|
||||
else :
|
||||
mode = e.change
|
||||
update()
|
||||
|
||||
i3.on("mode", on_mode)
|
||||
|
||||
#def on_window_focus(i3, e):
|
||||
# global container
|
||||
# container = e.container
|
||||
# update()
|
||||
#
|
||||
#i3.on("window::focus", on_window_focus)
|
||||
|
||||
def on_workspace_focus(i3, e):
|
||||
global workspaces
|
||||
workspaces = i3.get_workspaces()
|
||||
update()
|
||||
|
||||
i3.on("workspace::focus", on_workspace_focus)
|
||||
|
||||
# Starting
|
||||
|
||||
update()
|
||||
|
||||
|
||||
i3.main()
|
||||
# TODO CPU provider
|
||||
# TODO RAM provider
|
||||
# TODO Temperature provider
|
||||
# TODO Disk space provider
|
||||
# TODO Screen (connected, autorandr configuration, bbswitch) provider
|
||||
# TODO Unlocked keys provider
|
||||
# TODO Mail provider
|
||||
Bar.addSectionAll(TodoProvider(dir='~/.vdirsyncer/currentCalendars/'), BarGroupType.RIGHT)
|
||||
Bar.addSectionAll(NetworkProvider(), BarGroupType.RIGHT)
|
||||
Bar.addSectionAll(PulseaudioProvider(), BarGroupType.RIGHT)
|
||||
Bar.addSectionAll(BatteryProvider(), BarGroupType.RIGHT)
|
||||
Bar.addSectionAll(TimeProvider(), BarGroupType.RIGHT)
|
||||
|
|
|
@ -1,23 +1,24 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import enum
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
import subprocess
|
||||
|
||||
# TODO Update strategies (periodic, inotify file)
|
||||
# TODO Section order (priority system maybe ?)
|
||||
# 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
|
||||
|
||||
|
@ -28,24 +29,31 @@ class Bar:
|
|||
"""
|
||||
|
||||
# Constants
|
||||
FONT = "DejaVu Sans Mono for Powerline"
|
||||
FONTS = ["DejaVu Sans Mono for Powerline", "Font Awesome"]
|
||||
|
||||
@staticmethod
|
||||
def init():
|
||||
Section.init()
|
||||
|
||||
cmd = ['lemonbar', '-f', Bar.FONT, '-b']
|
||||
cmd = ['lemonbar', '-b']
|
||||
for font in Bar.FONTS:
|
||||
cmd += ["-f", font]
|
||||
Bar.process = subprocess.Popen(cmd, stdin=subprocess.PIPE)
|
||||
|
||||
# Debug
|
||||
# Bar(0)
|
||||
Bar(1)
|
||||
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) + "}"
|
||||
|
@ -94,7 +102,7 @@ class Bar:
|
|||
# Color for empty sections
|
||||
Bar.string += BarGroup.color(*Section.EMPTY)
|
||||
|
||||
print(Bar.string)
|
||||
# print(Bar.string)
|
||||
Bar.process.stdin.write(bytes(Bar.string + '\n', 'utf-8'))
|
||||
Bar.process.stdin.flush()
|
||||
|
||||
|
@ -127,7 +135,12 @@ class BarGroup:
|
|||
|
||||
def addSection(self, section):
|
||||
self.sections.append(section)
|
||||
section.parents.add(self)
|
||||
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}"}
|
||||
|
||||
|
@ -155,7 +168,7 @@ class BarGroup:
|
|||
if self.groupType == BarGroupType.LEFT:
|
||||
oSec = secs[s + 1] if s < lenS - 1 else None
|
||||
else:
|
||||
oSec = secs[s - 1] if s > 1 else None
|
||||
oSec = secs[s - 1] if s > 0 else None
|
||||
oTheme = Section.THEMES[oSec.theme] \
|
||||
if oSec is not None else Section.EMPTY
|
||||
|
||||
|
@ -164,7 +177,13 @@ class BarGroup:
|
|||
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))
|
||||
|
@ -189,21 +208,29 @@ class BarGroup:
|
|||
|
||||
@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:
|
||||
time.sleep(0.1)
|
||||
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:
|
||||
|
@ -218,19 +245,18 @@ class Section:
|
|||
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 = SectionThread(daemon=True)
|
||||
Section.updateThread.start()
|
||||
|
||||
#: Sections that do not have their destination size
|
||||
sizeChanging = set()
|
||||
somethingChanged = threading.Event()
|
||||
updateThread = None
|
||||
|
||||
def __init__(self, theme=0):
|
||||
#: Displayed section
|
||||
#: Note: A section can be empty and displayed!
|
||||
|
@ -252,10 +278,21 @@ class 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:
|
||||
|
@ -267,8 +304,15 @@ class Section:
|
|||
|
||||
def updateText(self, text):
|
||||
if len(text):
|
||||
self.dstText = ' {} '.format(text)
|
||||
self.dstSize = len(self.dstText)
|
||||
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
|
||||
|
||||
|
@ -331,6 +375,9 @@ class Section:
|
|||
|
||||
Section.somethingChanged.clear()
|
||||
|
||||
@staticmethod
|
||||
def ramp(p, ramp=" ▁▂▃▄▅▆▇█"):
|
||||
return ramp[round(p * (len(ramp)-1))]
|
||||
|
||||
if __name__ == '__main__':
|
||||
Bar.init()
|
29
config/lemonbar/net.py
Executable file
29
config/lemonbar/net.py
Executable file
|
@ -0,0 +1,29 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import psutil
|
||||
import subprocess
|
||||
|
||||
netStats = psutil.net_if_stats()
|
||||
netAddrs = psutil.net_if_addrs()
|
||||
netIO = psutil.net_io_counters(pernic=True)
|
||||
|
||||
for iface in netStats.keys():
|
||||
if not netStats[iface].isup or iface.startswith('lo'):
|
||||
continue
|
||||
|
||||
ssid = ''
|
||||
if iface.startswith('eth') or iface.startswith('enp'):
|
||||
icon = 'E'
|
||||
elif iface.startswith('wlan') or iface.startswith('wlp'):
|
||||
icon = 'W'
|
||||
cmd = ["iwgetid", iface, "--raw"]
|
||||
p = subprocess.run(cmd, stdout=subprocess.PIPE)
|
||||
ssid = p.stdout.strip().decode()
|
||||
# TODO Real icons
|
||||
# TODO USB tethering
|
||||
# TODO tan / tun
|
||||
else:
|
||||
icon = '?'
|
||||
|
||||
print(icon, iface, ssid)
|
||||
print(netIO[iface])
|
161
config/lemonbar/oldbar.py
Executable file
161
config/lemonbar/oldbar.py
Executable file
|
@ -0,0 +1,161 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Debugging script
|
||||
"""
|
||||
|
||||
import i3ipc
|
||||
import os
|
||||
import psutil
|
||||
# import alsaaudio
|
||||
from time import time
|
||||
import subprocess
|
||||
|
||||
i3 = i3ipc.Connection()
|
||||
lemonbar = subprocess.Popen(['lemonbar', '-b'], stdin=subprocess.PIPE)
|
||||
|
||||
# Utils
|
||||
def upChart(p):
|
||||
block = ' ▁▂▃▄▅▆▇█'
|
||||
return block[round(p * (len(block)-1))]
|
||||
|
||||
def humanSizeOf(num, suffix='B'): # TODO Credit
|
||||
for unit in ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']:
|
||||
if abs(num) < 1024.0:
|
||||
return "%3.0f%2s%s" % (num, unit, suffix)
|
||||
num /= 1024.0
|
||||
return "%.0f%2s%s" % (num, 'Yi', suffix)
|
||||
|
||||
# Values
|
||||
mode = ''
|
||||
container = i3.get_tree().find_focused()
|
||||
workspaces = i3.get_workspaces()
|
||||
outputs = i3.get_outputs()
|
||||
|
||||
username = os.environ['USER']
|
||||
hostname = os.environ['HOSTNAME']
|
||||
if '-' in hostname:
|
||||
hostname = hostname.split('-')[-1]
|
||||
|
||||
oldNetIO = dict()
|
||||
oldTime = time()
|
||||
|
||||
def update():
|
||||
activeOutputs = sorted(sorted(list(filter(lambda o: o.active, outputs)), key=lambda o: o.rect.y), key=lambda o: o.rect.x)
|
||||
z = ''
|
||||
for aOutput in range(len(activeOutputs)):
|
||||
output = activeOutputs[aOutput]
|
||||
# Mode || Workspaces
|
||||
t = []
|
||||
if (mode != ''):
|
||||
t.append(mode)
|
||||
else:
|
||||
t.append(' '.join([(w.name.upper() if w.focused else w.name) for w in workspaces if w.output == output.name]))
|
||||
|
||||
# Windows Title
|
||||
#if container:
|
||||
# t.append(container.name)
|
||||
|
||||
# CPU
|
||||
t.append('C' + ''.join([upChart(p/100) for p in psutil.cpu_percent(percpu=True)]))
|
||||
|
||||
# Memory
|
||||
t.append('M' + str(round(psutil.virtual_memory().percent)) + '% ' +
|
||||
'S' + str(round(psutil.swap_memory().percent)) + '%')
|
||||
|
||||
# Disks
|
||||
d = []
|
||||
for disk in psutil.disk_partitions():
|
||||
e = ''
|
||||
if disk.device.startswith('/dev/sd'):
|
||||
e += 'S' + disk.device[-2:].upper()
|
||||
elif disk.device.startswith('/dev/mmcblk'):
|
||||
e += 'M' + disk.device[-3] + disk.device[-1]
|
||||
else:
|
||||
e += '?'
|
||||
e += ' '
|
||||
e += str(round(psutil.disk_usage(disk.mountpoint).percent)) + '%'
|
||||
d.append(e)
|
||||
t.append(' '.join(d))
|
||||
|
||||
# Network
|
||||
netStats = psutil.net_if_stats()
|
||||
netIO = psutil.net_io_counters(pernic=True)
|
||||
net = []
|
||||
for iface in filter(lambda i: i != 'lo' and netStats[i].isup, netStats.keys()):
|
||||
s = ''
|
||||
if iface.startswith('eth'):
|
||||
s += 'E'
|
||||
elif iface.startswith('wlan'):
|
||||
s += 'W'
|
||||
else:
|
||||
s += '?'
|
||||
|
||||
s += ' '
|
||||
now = time()
|
||||
global oldNetIO, oldTime
|
||||
|
||||
sent = ((oldNetIO[iface].bytes_sent if iface in oldNetIO else 0) - (netIO[iface].bytes_sent if iface in netIO else 0)) / (oldTime - now)
|
||||
recv = ((oldNetIO[iface].bytes_recv if iface in oldNetIO else 0) - (netIO[iface].bytes_recv if iface in netIO else 0)) / (oldTime - now)
|
||||
s += '↓' + humanSizeOf(abs(recv), 'B/s') + ' ↑' + humanSizeOf(abs(sent), 'B/s')
|
||||
|
||||
oldNetIO = netIO
|
||||
oldTime = now
|
||||
|
||||
net.append(s)
|
||||
t.append(' '.join(net))
|
||||
|
||||
# Battery
|
||||
if os.path.isdir('/sys/class/power_supply/BAT0'):
|
||||
with open('/sys/class/power_supply/BAT0/charge_now') as f:
|
||||
charge_now = int(f.read())
|
||||
with open('/sys/class/power_supply/BAT0/charge_full_design') as f:
|
||||
charge_full = int(f.read())
|
||||
t.append('B' + str(round(100*charge_now/charge_full)) + '%')
|
||||
|
||||
# Volume
|
||||
# t.append('V ' + str(alsaaudio.Mixer('Master').getvolume()[0]) + '%')
|
||||
|
||||
t.append(username + '@' + hostname)
|
||||
|
||||
# print(' - '.join(t))
|
||||
# t = [output.name]
|
||||
|
||||
z += ' - '.join(t) + '%{S' + str(aOutput + 1) + '}'
|
||||
#lemonbar.stdin.write(bytes(' - '.join(t), 'utf-8'))
|
||||
#lemonbar.stdin.write(bytes('%{S' + str(aOutput + 1) + '}', 'utf-8'))
|
||||
|
||||
lemonbar.stdin.write(bytes(z+'\n', 'utf-8'))
|
||||
lemonbar.stdin.flush()
|
||||
|
||||
# Event listeners
|
||||
def on_mode(i3, e):
|
||||
global mode
|
||||
if (e.change == 'default'):
|
||||
mode = ''
|
||||
else :
|
||||
mode = e.change
|
||||
update()
|
||||
|
||||
i3.on("mode", on_mode)
|
||||
|
||||
#def on_window_focus(i3, e):
|
||||
# global container
|
||||
# container = e.container
|
||||
# update()
|
||||
#
|
||||
#i3.on("window::focus", on_window_focus)
|
||||
|
||||
def on_workspace_focus(i3, e):
|
||||
global workspaces
|
||||
workspaces = i3.get_workspaces()
|
||||
update()
|
||||
|
||||
i3.on("workspace::focus", on_workspace_focus)
|
||||
|
||||
# Starting
|
||||
|
||||
update()
|
||||
|
||||
|
||||
i3.main()
|
397
config/lemonbar/providers.py
Executable file
397
config/lemonbar/providers.py
Executable file
|
@ -0,0 +1,397 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import datetime
|
||||
from updaters import *
|
||||
from display import *
|
||||
import pulsectl
|
||||
import psutil
|
||||
import subprocess
|
||||
import socket
|
||||
import ipaddress
|
||||
import logging
|
||||
import coloredlogs
|
||||
import json
|
||||
import i3ipc
|
||||
from pprint import pprint
|
||||
|
||||
coloredlogs.install(level='DEBUG', fmt='%(levelname)s %(message)s')
|
||||
log = logging.getLogger()
|
||||
|
||||
|
||||
def humanSize(num):
|
||||
"""
|
||||
Returns a string of width 3+3
|
||||
"""
|
||||
for unit in ('B ','KiB','MiB','GiB','TiB','PiB','EiB','ZiB'):
|
||||
if abs(num) < 1000:
|
||||
if num >= 10:
|
||||
return "{:3d}{}".format(int(num), unit)
|
||||
else:
|
||||
return "{:.1f}{}".format(num, unit)
|
||||
num /= 1024.0
|
||||
return "{:d}YiB".format(num)
|
||||
|
||||
|
||||
class TimeProvider(Section, PeriodicUpdater):
|
||||
def fetcher(self):
|
||||
now = datetime.datetime.now()
|
||||
return now.strftime('%d/%m/%y %H:%M:%S')
|
||||
|
||||
def __init__(self):
|
||||
PeriodicUpdater.__init__(self)
|
||||
Section.__init__(self)
|
||||
self.changeInterval(1)
|
||||
|
||||
|
||||
class BatteryProvider(Section, PeriodicUpdater):
|
||||
|
||||
RAMP = ""
|
||||
|
||||
def fetcher(self):
|
||||
with open(self.batdir + 'status') as f:
|
||||
status = f.read().strip()
|
||||
if status == "Full":
|
||||
return ""
|
||||
elif status == "Discharging":
|
||||
icon = ""
|
||||
elif status == "Charging":
|
||||
icon = ""
|
||||
else:
|
||||
log.warn("Unknwon battery status: {}".format(status))
|
||||
icon = "?"
|
||||
with open(self.batdir + 'capacity') as f:
|
||||
capacity = int(f.read())
|
||||
icon += self.ramp(capacity/100, self.RAMP)
|
||||
return '{} {}%'.format(icon, capacity)
|
||||
|
||||
def __init__(self, battery='BAT0'):
|
||||
PeriodicUpdater.__init__(self)
|
||||
Section.__init__(self)
|
||||
|
||||
self.batdir = '/sys/class/power_supply/{}/'.format(battery)
|
||||
assert os.path.isdir(self.batdir)
|
||||
|
||||
self.changeInterval(5)
|
||||
|
||||
|
||||
class PulseaudioProvider(Section, ThreadedUpdater):
|
||||
def __init__(self):
|
||||
ThreadedUpdater.__init__(self)
|
||||
Section.__init__(self)
|
||||
self.pulseEvents = pulsectl.Pulse('event-handler')
|
||||
|
||||
self.pulseEvents.event_mask_set(pulsectl.PulseEventMaskEnum.sink)
|
||||
self.pulseEvents.event_callback_set(self.handleEvent)
|
||||
self.start()
|
||||
self.refreshData()
|
||||
|
||||
|
||||
def fetcher(self):
|
||||
sinks = []
|
||||
with pulsectl.Pulse('list-sinks') as pulse:
|
||||
for sink in pulse.sink_list():
|
||||
vol = pulse.volume_get_all_chans(sink)
|
||||
if vol > 1:
|
||||
vol = 1
|
||||
|
||||
if sink.port_active.name == "analog-output-headphones":
|
||||
icon = ""
|
||||
elif sink.port_active.name == "analog-output-speaker":
|
||||
icon = ""
|
||||
else:
|
||||
icon = "?"
|
||||
|
||||
ramp = "" if sink.mute else (" " + self.ramp(vol))
|
||||
sinks.append(icon + ramp)
|
||||
return " ".join(sinks)
|
||||
|
||||
def loop(self):
|
||||
self.pulseEvents.event_listen()
|
||||
|
||||
def handleEvent(self, ev):
|
||||
self.refreshData()
|
||||
|
||||
class NetworkProviderSection(Section, Updater):
|
||||
THEME = 5
|
||||
|
||||
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
|
||||
|
||||
# Get addresses
|
||||
ipv4 = None
|
||||
ipv6 = None
|
||||
for address in self.parent.addrs[self.iface]:
|
||||
if address.family == socket.AF_INET:
|
||||
ipv4 = address
|
||||
elif address.family == socket.AF_INET6:
|
||||
ipv6 = address
|
||||
if ipv4 is None and ipv6 is None:
|
||||
return text
|
||||
|
||||
# Set icon
|
||||
if self.iface.startswith('eth') or self.iface.startswith('enp'):
|
||||
if 'u' in self.iface:
|
||||
text = ''
|
||||
else:
|
||||
text = ''
|
||||
elif self.iface.startswith('wlan') or self.iface.startswith('wlp'):
|
||||
text = ''
|
||||
cmd = ["iwgetid", self.iface, "--raw"]
|
||||
p = subprocess.run(cmd, stdout=subprocess.PIPE)
|
||||
|
||||
text += ' ' + p.stdout.strip().decode()
|
||||
elif self.iface.startswith('tun') or self.iface.startswith('tap'):
|
||||
text = ''
|
||||
else:
|
||||
text = '?'
|
||||
|
||||
if self.showAddress:
|
||||
if ipv4:
|
||||
netStrFull = '{}/{}'.format(ipv4.address, ipv4.netmask)
|
||||
addr = ipaddress.IPv4Network(netStrFull, strict=False)
|
||||
text += ' {}/{}'.format(ipv4.address, addr.prefixlen)
|
||||
# TODO IPV6
|
||||
# if ipv6:
|
||||
# text += ' ' + ipv6.address
|
||||
|
||||
if self.showSpeed:
|
||||
recvDiff = self.parent.IO[self.iface].bytes_recv \
|
||||
- self.parent.prevIO[self.iface].bytes_recv
|
||||
sentDiff = self.parent.IO[self.iface].bytes_sent \
|
||||
- self.parent.prevIO[self.iface].bytes_sent
|
||||
recvDiff /= self.parent.dt
|
||||
sentDiff /= self.parent.dt
|
||||
text += ' ↓{}↑{}'.format(humanSize(recvDiff), humanSize(sentDiff))
|
||||
|
||||
if self.showTransfer:
|
||||
text += ' ⇓{}⇑{}'.format(
|
||||
humanSize(self.parent.IO[self.iface].bytes_recv),
|
||||
humanSize(self.parent.IO[self.iface].bytes_sent))
|
||||
|
||||
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
|
||||
self.showAddress = state >= 1
|
||||
self.showSpeed = state >= 2
|
||||
self.showTransfer = state >= 3
|
||||
|
||||
def __init__(self, iface, parent):
|
||||
Section.__init__(self, theme=self.THEME)
|
||||
self.iface = iface
|
||||
self.parent = parent
|
||||
self.changeState(1)
|
||||
self.refreshData()
|
||||
|
||||
|
||||
class NetworkProvider(Section, PeriodicUpdater):
|
||||
def fetchData(self):
|
||||
self.prev = self.last
|
||||
self.prevIO = self.IO
|
||||
|
||||
self.stats = psutil.net_if_stats()
|
||||
self.addrs = psutil.net_if_addrs()
|
||||
self.IO = psutil.net_io_counters(pernic=True)
|
||||
self.ifaces = self.stats.keys()
|
||||
|
||||
self.last = time.perf_counter()
|
||||
self.dt = self.last - self.prev
|
||||
|
||||
def refreshData(self):
|
||||
self.fetchData()
|
||||
|
||||
lastSection = self
|
||||
for iface in sorted(list(self.ifaces)):
|
||||
if iface not in self.sections.keys():
|
||||
section = NetworkProviderSection(iface, self)
|
||||
lastSection.appendAfter(section)
|
||||
self.sections[iface] = section
|
||||
else:
|
||||
section = self.sections[iface]
|
||||
section.refreshData()
|
||||
lastSection = section
|
||||
|
||||
def addParent(self, parent):
|
||||
self.parents.add(parent)
|
||||
self.refreshData()
|
||||
|
||||
def __init__(self):
|
||||
PeriodicUpdater.__init__(self)
|
||||
Section.__init__(self)
|
||||
|
||||
self.sections = dict()
|
||||
self.last = 0
|
||||
self.IO = dict()
|
||||
self.fetchData()
|
||||
|
||||
|
||||
class TodoProvider(Section, InotifyUpdater):
|
||||
# TODO OPT/UX Maybe we could get more data from the todoman python module
|
||||
# TODO OPT Specific callback for specific directory
|
||||
|
||||
def updateCalendarList(self):
|
||||
calendars = sorted(os.listdir(self.dir))
|
||||
for calendar in calendars:
|
||||
# If the calendar wasn't in the list
|
||||
if calendar not in self.calendars:
|
||||
self.addPath(os.path.join(self.dir, calendar), refresh=False)
|
||||
self.calendars = calendars
|
||||
|
||||
def __init__(self, dir):
|
||||
"""
|
||||
:parm str dir: [main]path value in todoman.conf
|
||||
"""
|
||||
InotifyUpdater.__init__(self)
|
||||
Section.__init__(self)
|
||||
self.dir = os.path.realpath(os.path.expanduser(dir))
|
||||
assert os.path.isdir(self.dir)
|
||||
|
||||
self.calendars = []
|
||||
self.addPath(self.dir)
|
||||
|
||||
def getName(self, calendar):
|
||||
path = os.path.join(self.dir, calendar, 'displayname')
|
||||
with open(path, 'r') as f:
|
||||
name = f.read().strip()
|
||||
return name
|
||||
|
||||
def getColor(self, calendar):
|
||||
path = os.path.join(self.dir, calendar, 'color')
|
||||
with open(path, 'r') as f:
|
||||
color = f.read().strip()
|
||||
return color
|
||||
|
||||
def countUndone(self, calendar):
|
||||
cmd = ["todo", "--porcelain", "list", self.getName(calendar)]
|
||||
proc = subprocess.run(cmd, stdout=subprocess.PIPE)
|
||||
data = json.loads(proc.stdout)
|
||||
return len(data)
|
||||
|
||||
def fetcher(self):
|
||||
text = []
|
||||
self.updateCalendarList()
|
||||
for calendar in self.calendars:
|
||||
c = self.countUndone(calendar)
|
||||
if c > 0:
|
||||
color = self.getColor(calendar)
|
||||
text += [' ']
|
||||
text += [{"text": str(c), "fgColor": color}]
|
||||
if len(text):
|
||||
return [''] + text
|
||||
else:
|
||||
return ''
|
||||
|
||||
class I3Provider(Section, ThreadedUpdater):
|
||||
# TODO Multi-screen
|
||||
|
||||
THEME_NORMAL = 0
|
||||
THEME_FOCUSED = 2
|
||||
THEME_URGENT = 1
|
||||
THEME_MODE = 1
|
||||
|
||||
def setName(self, section, origName):
|
||||
# TODO Custom names
|
||||
if origName:
|
||||
section.fullName = origName
|
||||
else:
|
||||
section.fullName = ''
|
||||
section.updateText(section.fullName)
|
||||
|
||||
def initialPopulation(self, parent):
|
||||
"""
|
||||
Called on init
|
||||
Can't reuse addWorkspace since i3.get_workspaces() gives dict and not
|
||||
ConObjects
|
||||
"""
|
||||
workspaces = self.i3.get_workspaces()
|
||||
lastSection = self.modeSection
|
||||
for workspace in workspaces:
|
||||
# if parent.display != workspace["display"]:
|
||||
# continue
|
||||
theme = self.THEME_FOCUSED if workspace["focused"] \
|
||||
else (self.THEME_URGENT if workspace["urgent"]
|
||||
else self.THEME_NORMAL)
|
||||
section = Section(theme=theme)
|
||||
parent.addSectionAfter(lastSection, section)
|
||||
self.setName(section, workspace["name"])
|
||||
self.sections[workspace["num"]] = section
|
||||
lastSection = section
|
||||
|
||||
def on_workspace(self, i3, e):
|
||||
print(304, e.change)
|
||||
|
||||
def on_workspace_init(self, i3, e):
|
||||
workspace = e.current
|
||||
i = workspace.num
|
||||
if i in self.sections:
|
||||
section = self.sections[i]
|
||||
else:
|
||||
while i not in self.sections.keys() and i > 0:
|
||||
i -= 1
|
||||
prevSection = self.sections[i] if i != 0 else self.modeSection
|
||||
section = Section()
|
||||
self.sections[workspace.num] = section
|
||||
prevSection.appendAfter(section)
|
||||
self.setName(section, workspace.name)
|
||||
|
||||
def on_workspace_empty(self, i3, e):
|
||||
workspace = e.current
|
||||
section = self.sections[workspace.num]
|
||||
self.setName(section, None)
|
||||
|
||||
def on_workspace_focus(self, i3, e):
|
||||
self.sections[e.current.num].updateTheme(self.THEME_FOCUSED)
|
||||
self.sections[e.old.num].updateTheme(self.THEME_NORMAL)
|
||||
|
||||
def on_workspace_urgent(self, i3, e):
|
||||
self.sections[e.current.num].updateTheme(self.THEME_URGENT)
|
||||
|
||||
def on_workspace_rename(self, i3, e):
|
||||
self.sections[e.current.num].updateText(e.name)
|
||||
|
||||
def on_mode(self, i3, e):
|
||||
if e.change == 'default':
|
||||
self.modeSection.updateText('')
|
||||
for section in self.sections.values():
|
||||
section.updateText(section.fullName)
|
||||
else:
|
||||
self.modeSection.updateText(e.change)
|
||||
for section in self.sections.values():
|
||||
section.updateText('')
|
||||
|
||||
def __init__(self):
|
||||
ThreadedUpdater.__init__(self)
|
||||
Section.__init__(self)
|
||||
|
||||
self.i3 = i3ipc.Connection()
|
||||
self.sections = dict()
|
||||
self.i3.on("workspace::init", self.on_workspace_init)
|
||||
self.i3.on("workspace::focus", self.on_workspace_focus)
|
||||
self.i3.on("workspace::empty", self.on_workspace_empty)
|
||||
self.i3.on("workspace::urgent", self.on_workspace_urgent)
|
||||
self.i3.on("workspace::rename", self.on_workspace_rename)
|
||||
# TODO Un-handled/tested: reload, rename, restored, move
|
||||
|
||||
self.i3.on("mode", self.on_mode)
|
||||
|
||||
self.modeSection = Section(theme=self.THEME_MODE)
|
||||
self.start()
|
||||
|
||||
def addParent(self, parent):
|
||||
self.parents.add(parent)
|
||||
parent.addSection(self.modeSection)
|
||||
self.initialPopulation(parent)
|
||||
|
||||
def loop(self):
|
||||
self.i3.main()
|
194
config/lemonbar/updaters.py
Executable file
194
config/lemonbar/updaters.py
Executable file
|
@ -0,0 +1,194 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import math
|
||||
import functools
|
||||
import threading
|
||||
import pyinotify
|
||||
import os
|
||||
import time
|
||||
|
||||
# TODO Multiple providers for the same section
|
||||
|
||||
|
||||
class Updater:
|
||||
@staticmethod
|
||||
def init():
|
||||
PeriodicUpdater.init()
|
||||
InotifyUpdater.init()
|
||||
|
||||
def updateText(self, text):
|
||||
print(text)
|
||||
|
||||
def fetcher(self):
|
||||
return "{} refreshed".format(self)
|
||||
|
||||
def refreshData(self):
|
||||
data = self.fetcher()
|
||||
self.updateText(data)
|
||||
|
||||
|
||||
class PeriodicUpdaterThread(threading.Thread):
|
||||
def run(self):
|
||||
counter = 0
|
||||
while True:
|
||||
if PeriodicUpdater.intervalsChanged \
|
||||
.wait(timeout=PeriodicUpdater.intervalStep):
|
||||
# ↑ sleeps here
|
||||
PeriodicUpdater.intervalsChanged.clear()
|
||||
counter = 0
|
||||
for providerList in PeriodicUpdater.intervals.copy().values():
|
||||
for provider in providerList.copy():
|
||||
provider.refreshData()
|
||||
else:
|
||||
counter += PeriodicUpdater.intervalStep
|
||||
counter = counter % PeriodicUpdater.intervalLoop
|
||||
for interval in PeriodicUpdater.intervals.keys():
|
||||
if counter % interval == 0:
|
||||
for provider in PeriodicUpdater.intervals[interval]:
|
||||
provider.refreshData()
|
||||
|
||||
|
||||
class PeriodicUpdater(Updater):
|
||||
"""
|
||||
Needs to call :func:`PeriodicUpdater.changeInterval` in `__init__`
|
||||
"""
|
||||
|
||||
intervals = dict()
|
||||
intervalStep = None
|
||||
intervalLoop = None
|
||||
updateThread = PeriodicUpdaterThread(daemon=True)
|
||||
intervalsChanged = threading.Event()
|
||||
|
||||
@staticmethod
|
||||
def gcds(*args):
|
||||
return functools.reduce(math.gcd, args)
|
||||
|
||||
@staticmethod
|
||||
def lcm(a, b):
|
||||
"""Return lowest common multiple."""
|
||||
return a * b // math.gcd(a, b)
|
||||
|
||||
@staticmethod
|
||||
def lcms(*args):
|
||||
"""Return lowest common multiple."""
|
||||
return functools.reduce(PeriodicUpdater.lcm, args)
|
||||
|
||||
@staticmethod
|
||||
def updateIntervals():
|
||||
intervalsList = list(PeriodicUpdater.intervals.keys())
|
||||
PeriodicUpdater.intervalStep = PeriodicUpdater.gcds(*intervalsList)
|
||||
PeriodicUpdater.intervalLoop = PeriodicUpdater.lcms(*intervalsList)
|
||||
PeriodicUpdater.intervalsChanged.set()
|
||||
|
||||
@staticmethod
|
||||
def init():
|
||||
PeriodicUpdater.updateThread.start()
|
||||
|
||||
def __init__(self):
|
||||
Updater.__init__(self)
|
||||
self.interval = None
|
||||
|
||||
def changeInterval(self, interval):
|
||||
assert isinstance(interval, int)
|
||||
|
||||
if self.interval is not None:
|
||||
PeriodicUpdater.intervals[self.interval].remove(self)
|
||||
|
||||
self.interval = interval
|
||||
|
||||
if interval not in PeriodicUpdater.intervals:
|
||||
PeriodicUpdater.intervals[interval] = set()
|
||||
PeriodicUpdater.intervals[interval].add(self)
|
||||
|
||||
PeriodicUpdater.updateIntervals()
|
||||
|
||||
|
||||
class InotifyUpdaterEventHandler(pyinotify.ProcessEvent):
|
||||
|
||||
def process_default(self, event):
|
||||
# DEBUG
|
||||
# from pprint import pprint
|
||||
# pprint(event.__dict__)
|
||||
# return
|
||||
|
||||
assert event.path in InotifyUpdater.paths
|
||||
|
||||
if 0 in InotifyUpdater.paths[event.path]:
|
||||
for provider in InotifyUpdater.paths[event.path][0]:
|
||||
provider.refreshData()
|
||||
|
||||
if event.name in InotifyUpdater.paths[event.path]:
|
||||
for provider in InotifyUpdater.paths[event.path][event.name]:
|
||||
provider.refreshData()
|
||||
|
||||
|
||||
class InotifyUpdater(Updater):
|
||||
"""
|
||||
Needs to call :func:`PeriodicUpdater.changeInterval` in `__init__`
|
||||
"""
|
||||
|
||||
wm = pyinotify.WatchManager()
|
||||
paths = dict()
|
||||
|
||||
@staticmethod
|
||||
def init():
|
||||
notifier = pyinotify.ThreadedNotifier(InotifyUpdater.wm,
|
||||
InotifyUpdaterEventHandler())
|
||||
notifier.start()
|
||||
|
||||
# TODO Mask for folders
|
||||
MASK = pyinotify.IN_CREATE | pyinotify.IN_MODIFY | pyinotify.IN_DELETE
|
||||
|
||||
def addPath(self, path, refresh=True):
|
||||
path = os.path.realpath(os.path.expanduser(path))
|
||||
|
||||
# Detect if file or folder
|
||||
if os.path.isdir(path):
|
||||
self.dirpath = path
|
||||
# 0: Directory watcher
|
||||
self.filename = 0
|
||||
elif os.path.isfile(path):
|
||||
self.dirpath = os.path.dirname(path)
|
||||
self.filename = os.path.basename(path)
|
||||
else:
|
||||
raise FileNotFoundError("No such file or directory: '{}'"
|
||||
.format(path))
|
||||
|
||||
# Register watch action
|
||||
if self.dirpath not in InotifyUpdater.paths:
|
||||
InotifyUpdater.paths[self.dirpath] = dict()
|
||||
if self.filename not in InotifyUpdater.paths[self.dirpath]:
|
||||
InotifyUpdater.paths[self.dirpath][self.filename] = set()
|
||||
InotifyUpdater.paths[self.dirpath][self.filename].add(self)
|
||||
|
||||
# Add watch
|
||||
InotifyUpdater.wm.add_watch(self.dirpath, InotifyUpdater.MASK)
|
||||
|
||||
if refresh:
|
||||
self.refreshData()
|
||||
|
||||
|
||||
class ThreadedUpdaterThread(threading.Thread):
|
||||
def __init__(self, updater, *args, **kwargs):
|
||||
self.updater = updater
|
||||
threading.Thread.__init__(self, *args, **kwargs)
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
self.updater.loop()
|
||||
|
||||
|
||||
class ThreadedUpdater(Updater):
|
||||
"""
|
||||
Must implement loop(), and call start()
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.thread = ThreadedUpdaterThread(self, daemon=True)
|
||||
|
||||
def loop(self):
|
||||
self.refreshData()
|
||||
time.sleep(10)
|
||||
|
||||
def start(self):
|
||||
self.thread.start()
|
10
config/lemonbar/x.py
Executable file
10
config/lemonbar/x.py
Executable file
|
@ -0,0 +1,10 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import Xlib.display
|
||||
|
||||
dis = Xlib.display.Display()
|
||||
|
||||
nb = dis.screen_count()
|
||||
|
||||
for s in range(nb):
|
||||
print(s)
|
|
@ -76,7 +76,7 @@ enable-ipc = true
|
|||
inherit = bar/base
|
||||
|
||||
modules-center = mpd
|
||||
modules-right = mail todo vpncheck eth wlan bbswitch xbacklight volume battery shortdate
|
||||
modules-right = cpu memory temperature mail todo vpncheck eth wlan bbswitch xbacklight volume battery shortdate
|
||||
|
||||
tray-position = right
|
||||
tray-padding = 2
|
||||
|
|
|
@ -2,4 +2,11 @@
|
|||
|
||||
# Removes CRLF (^M or \r) from a file
|
||||
|
||||
sed -e "s/^M//" $1 -i
|
||||
#sed -e "s/^M//" "$1" -i
|
||||
|
||||
tmpfile=$(mktemp)
|
||||
|
||||
cp "$1" "$tmpfile"
|
||||
tr -d '\r' < "$tmpfile" > "$1"
|
||||
rm "$tmpfile"
|
||||
|
||||
|
|
Loading…
Reference in a new issue