Merge remote-tracking branch 'upstream/master'

This commit is contained in:
mw 2019-08-24 11:40:00 +02:00
commit 928895befe
33 changed files with 1859 additions and 680 deletions

View file

@ -2,7 +2,6 @@ sudo: false
language: python language: python
python: python:
- "2.7" - "2.7"
- "3.3"
- "3.4" - "3.4"
- "3.5" - "3.5"
- "3.6" - "3.6"

View file

@ -1,8 +1,9 @@
# Maintainer: Tobias Witek <tobi@tobi-wan-kenobi.at> # Maintainer: Tobias Witek <tobi@tobi-wan-kenobi.at>
# Contributor: Daniel M. Capella <polycitizen@gmail.com> # Contributor: Daniel M. Capella <polycitizen@gmail.com>
# Contributor: spookykidmm <https://github.com/spookykidmm>
pkgname=bumblebee-status pkgname=bumblebee-status
pkgver=1.4.2 pkgver=1.8.0
pkgrel=1 pkgrel=1
pkgdesc='Modular, theme-able status line generator for the i3 window manager' pkgdesc='Modular, theme-able status line generator for the i3 window manager'
arch=('any') arch=('any')
@ -22,9 +23,10 @@ optdepends=('xorg-xbacklight: to display a displays brightness'
'i3ipc-python: display titlebar' 'i3ipc-python: display titlebar'
'fakeroot: dependency of the pacman module' 'fakeroot: dependency of the pacman module'
'pytz: timezone conversion for datetimetz module' 'pytz: timezone conversion for datetimetz module'
'tzlocal: retrieve system timezone for datetimetz module') 'tzlocal: retrieve system timezone for datetimetz module'
)
source=("$pkgname-$pkgver.tar.gz::$url/archive/v$pkgver.tar.gz") source=("$pkgname-$pkgver.tar.gz::$url/archive/v$pkgver.tar.gz")
sha512sums=('3a66fc469dd3b081337c9e213a1b2262f25f30977ee6ef65b9fa5a8b6aa341637832d1a5dbb74e30d68e2824e0d19d7a911eb3390dc6062707a552f429b483e8') sha512sums=('b985e6619856519a92bd1a9d2a762997e8920a0ef5007f20063fcb2f9afeb193de67e8b0737182576f79ee19b2dd3e6388bb9b987480b7bc19b537f64e9b5685')
package() { package() {
install -d "$pkgdir"/usr/bin \ install -d "$pkgdir"/usr/bin \
@ -32,8 +34,8 @@ package() {
ln -s /usr/share/$pkgname/$pkgname "$pkgdir"/usr/bin/$pkgname ln -s /usr/share/$pkgname/$pkgname "$pkgdir"/usr/bin/$pkgname
cd $pkgname-$pkgver cd $pkgname-$pkgver
cp -a --parents $pkgname bumblebee/{,modules/}*.py themes/{,icons/}*.json \ cp -a --parents $pkgname bumblebee/{,modules/}*.py themes/{,icons/}*.json $pkgdir/usr/share/$pkgname
"$pkgdir"/usr/share/$pkgname cp -r bin $pkgdir/usr/share/$pkgname/
install -Dm644 LICENSE "$pkgdir"/usr/share/licenses/$pkgname/LICENSE install -Dm644 LICENSE "$pkgdir"/usr/share/licenses/$pkgname/LICENSE
} }

View file

@ -5,7 +5,7 @@
[![Test Coverage](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/coverage.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/coverage) [![Test Coverage](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/coverage.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/coverage)
[![Issue Count](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/issue_count.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status) [![Issue Count](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/issue_count.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status)
**Many, many thanks to all contributors! As of now, 41 of the modules are from various contributors (!), and only 18 from myself.** **Many, many thanks to all contributors! As of now, 47 of the modules are from various contributors (!), and only 19 from myself.**
![Solarized Powerline](https://github.com/tobi-wan-kenobi/bumblebee-status/blob/master/screenshots/themes/powerline-solarized.png) ![Solarized Powerline](https://github.com/tobi-wan-kenobi/bumblebee-status/blob/master/screenshots/themes/powerline-solarized.png)
@ -38,6 +38,10 @@ Explicitly unsupported Python versions: 3.2 (missing unicode literals)
# Arch Linux # Arch Linux
$ sudo pacman -S awesome-terminal-fonts $ sudo pacman -S awesome-terminal-fonts
# FreeBSD
$ sudo pkg install font-awesome
$ sudo pkg install py36-tzlocal py36-pytz py36-netifaces py36-psutil py36-requests #for dependencies
# Other # Other
# see https://github.com/gabrielelana/awesome-terminal-fonts # see https://github.com/gabrielelana/awesome-terminal-fonts
``` ```
@ -76,7 +80,10 @@ In your i3wm configuration, modify the *status_command* for your i3bar like this
``` ```
bar { bar {
status_command <path to bumblebee-status/bumblebee-status> -m <list of modules> -p <list of module parameters> -t <theme> status_command <path to bumblebee-status/bumblebee-status> \
-m <list of modules> \
-p <list of module parameters> \
-t <theme>
} }
``` ```
@ -180,7 +187,7 @@ Modules and commandline utilities are only required for modules, the core itself
* psutil (for the modules 'cpu', 'memory', 'traffic') * psutil (for the modules 'cpu', 'memory', 'traffic')
* netifaces (for the modules 'nic', 'traffic') * netifaces (for the modules 'nic', 'traffic')
* requests (for the modules 'weather', 'github', 'getcrypto', 'stock', 'currency') * requests (for the modules 'weather', 'github', 'getcrypto', 'stock', 'currency', 'sun')
* power (for the module 'battery') * power (for the module 'battery')
* dbus (for the module 'spotify') * dbus (for the module 'spotify')
* i3ipc (for the module 'title') * i3ipc (for the module 'title')
@ -188,6 +195,8 @@ Modules and commandline utilities are only required for modules, the core itself
* docker (for the module 'docker_ps') * docker (for the module 'docker_ps')
* pytz (for the module 'datetimetz') * pytz (for the module 'datetimetz')
* localtz (for the module 'datetimetz') * localtz (for the module 'datetimetz')
* suntime (for the module 'sun')
* feedparser (for the module 'rss')
# Required commandline utilities # Required commandline utilities
@ -210,6 +219,7 @@ Modules and commandline utilities are only required for modules, the core itself
* sensors (for module 'sensors', as fallback) * sensors (for module 'sensors', as fallback)
* zpool (for module 'zpool') * zpool (for module 'zpool')
* progress (for module 'progress') * progress (for module 'progress')
* i3exit (for module 'system')
# Examples # Examples
Here are some screenshots for all themes that currently exist: Here are some screenshots for all themes that currently exist:

View file

@ -84,7 +84,7 @@ class Config(bumblebee.store.Store):
parameters = [item for sub in self._args.parameters for item in sub] parameters = [item for sub in self._args.parameters for item in sub]
for param in parameters: for param in parameters:
key, value = param.split("=") key, value = param.split("=", 1)
self.set(key, value) self.set(key, value)
def modules(self): def modules(self):

View file

@ -41,6 +41,7 @@ class Module(object):
self.error = None self.error = None
self._next = int(time.time()) self._next = int(time.time())
self._default_interval = 0 self._default_interval = 0
self._engine = engine
self._configFile = None self._configFile = None
for cfg in [os.path.expanduser("~/.bumblebee-status.conf"), os.path.expanduser("~/.config/bumblebee-status.conf")]: for cfg in [os.path.expanduser("~/.bumblebee-status.conf"), os.path.expanduser("~/.config/bumblebee-status.conf")]:
@ -56,6 +57,9 @@ class Module(object):
if widgets: if widgets:
self._widgets = widgets if isinstance(widgets, list) else [widgets] self._widgets = widgets if isinstance(widgets, list) else [widgets]
def theme(self):
return self._engine.theme()
def widgets(self, widgets=None): def widgets(self, widgets=None):
"""Return the widgets to draw for this module""" """Return the widgets to draw for this module"""
if widgets: if widgets:
@ -160,6 +164,9 @@ class Engine(object):
self.input.start() self.input.start()
def theme(self):
return self._theme
def _toggle_minimize(self, event): def _toggle_minimize(self, event):
for module in self._modules: for module in self._modules:
widget = module.widget_by_id(event["instance"]) widget = module.widget_by_id(event["instance"])

View file

@ -25,15 +25,15 @@ def is_terminated():
def read_input(inp): def read_input(inp):
"""Read i3bar input and execute callbacks""" """Read i3bar input and execute callbacks"""
epoll = select.epoll() poll = select.poll()
epoll.register(sys.stdin.fileno(), select.EPOLLIN) poll.register(sys.stdin.fileno(), select.POLLIN)
log.debug("starting click event processing") log.debug("starting click event processing")
while inp.running: while inp.running:
if is_terminated(): if is_terminated():
return return
try: try:
events = epoll.poll(1) events = poll.poll(1000)
except Exception: except Exception:
continue continue
for fileno, event in events: for fileno, event in events:
@ -52,8 +52,7 @@ def read_input(inp):
except ValueError as e: except ValueError as e:
log.debug("failed to parse event: {}".format(e)) log.debug("failed to parse event: {}".format(e))
log.debug("exiting click event processing") log.debug("exiting click event processing")
epoll.unregister(sys.stdin.fileno()) poll.unregister(sys.stdin.fileno())
epoll.close()
inp.has_event = True inp.has_event = True
inp.clean_exit = True inp.clean_exit = True

View file

@ -29,11 +29,15 @@ class Module(bumblebee.engine.Module):
return len(packages) return len(packages)
return 0 return 0
@property
def _format(self):
return self.parameter("format", "Update Arch: {}")
def utilization(self, widget): def utilization(self, widget):
return 'Update Arch: {}'.format(self.packages) return self._format.format(self.packages)
def hidden(self): def hidden(self):
return self.check_updates() == 0 return self.check_updates() == 0
def update(self, widgets): def update(self, widgets):
self.packages = self.check_updates() self.packages = self.check_updates()

View file

@ -7,6 +7,7 @@ Parameters:
* battery.warning : Warning threshold in % of remaining charge (defaults to 20) * battery.warning : Warning threshold in % of remaining charge (defaults to 20)
* battery.critical : Critical threshold in % of remaining charge (defaults to 10) * battery.critical : Critical threshold in % of remaining charge (defaults to 10)
* battery.showdevice : If set to "true", add the device name to the widget (defaults to False) * battery.showdevice : If set to "true", add the device name to the widget (defaults to False)
* battery.decorate : If set to "false", hides additional icons (charging, etc.) (defaults to True)
""" """
import os import os
@ -47,6 +48,8 @@ class Module(bumblebee.engine.Module):
self.capacity(widget) self.capacity(widget)
while len(widgets) > 0: del widgets[0] while len(widgets) > 0: del widgets[0]
for widget in new_widgets: for widget in new_widgets:
if bumblebee.util.asbool(self.parameter("decorate", True)) == False:
widget.set("theme.exclude", "suffix")
widgets.append(widget) widgets.append(widget)
self._widgets = widgets self._widgets = widgets

View file

@ -17,8 +17,11 @@ Parameters:
from __future__ import absolute_import from __future__ import absolute_import
import datetime import datetime
import locale import locale
import pytz try:
import tzlocal import pytz
import tzlocal
except:
pass
import bumblebee.input import bumblebee.input
import bumblebee.output import bumblebee.output
import bumblebee.engine import bumblebee.engine
@ -40,7 +43,10 @@ class Module(bumblebee.engine.Module):
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE, cmd=self.next_tz) engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE, cmd=self.next_tz)
engine.input.register_callback(self, button=bumblebee.input.RIGHT_MOUSE, cmd=self.prev_tz) engine.input.register_callback(self, button=bumblebee.input.RIGHT_MOUSE, cmd=self.prev_tz)
self._fmt = self.parameter("format", default_format(self.name)) self._fmt = self.parameter("format", default_format(self.name))
self._timezones = self.parameter("timezone", tzlocal.get_localzone().zone).split(",") try:
self._timezones = self.parameter("timezone", tzlocal.get_localzone().zone).split(",")
except:
self._timezones = ""
self._current_tz = 0 self._current_tz = 0
l = locale.getdefaultlocale() l = locale.getdefaultlocale()
@ -54,10 +60,13 @@ class Module(bumblebee.engine.Module):
def get_time(self, widget): def get_time(self, widget):
try: try:
tz = pytz.timezone(self._timezones[self._current_tz].strip()) try:
retval = datetime.datetime.now(tz=tzlocal.get_localzone()).astimezone(tz).strftime(self._fmt) tz = pytz.timezone(self._timezones[self._current_tz].strip())
except pytz.exceptions.UnknownTimeZoneError: retval = datetime.datetime.now(tz=tzlocal.get_localzone()).astimezone(tz).strftime(self._fmt)
retval = "[Unknown timezone: {}]".format(self._timezones[self._current_tz].strip()) except pytz.exceptions.UnknownTimeZoneError:
retval = "[Unknown timezone: {}]".format(self._timezones[self._current_tz].strip())
except:
retval = "[n/a]"
enc = locale.getpreferredencoding() enc = locale.getpreferredencoding()
if hasattr(retval, "decode"): if hasattr(retval, "decode"):

View file

@ -0,0 +1,68 @@
# pylint: disable=C0111,R0903
"""Display HTTP status code
Parameters:
* http_status.label: Prefix label (optional)
* http_status.target: Target to retrieve the HTTP status from
* http_status.expect: Expected HTTP status
"""
from requests import head
import psutil
import bumblebee.input
import bumblebee.output
import bumblebee.engine
class Module(bumblebee.engine.Module):
UNK = "UNK"
def __init__(self, engine, config):
widget = bumblebee.output.Widget(full_text=self.output)
super(Module, self).__init__(engine, config, widget)
self._label = self.parameter("label")
self._target = self.parameter("target")
self._expect = self.parameter("expect", "200")
self._status = self.getStatus()
self._output = self.getOutput()
def labelize(self, s):
if self._label is None:
return s
return "{}: {}".format(self._label, s)
def getStatus(self):
try:
res = head(self._target)
except Exception:
return self.UNK
else:
status = str(res.status_code)
self._status = status
return status
def getOutput(self):
if self._status == self._expect:
return self.labelize(self._status)
else:
reason = " != {}".format(self._expect)
return self.labelize("{}{}".format(self._status, reason))
def output(self, widget):
return self._output
def update(self, widgets):
self.getStatus()
self._output = self.getOutput()
def state(self, widget):
if self._status == self.UNK:
return "warning"
if self._status != self._expect:
return "critical"
return self._output
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,114 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Displays network traffic
* No extra configuration needed
"""
import psutil
import netifaces
import bumblebee.input
import bumblebee.output
import bumblebee.engine
import bumblebee.util
WIDGET_NAME = 'network_traffic'
class Module(bumblebee.engine.Module):
"""Bumblebee main module """
def __init__(self, engine, config):
super(Module, self).__init__(engine, config)
try:
self._bandwidth = BandwidthInfo()
self._bytes_recv = self._bandwidth.bytes_recv()
self._bytes_sent = self._bandwidth.bytes_sent()
except Exception:
""" We do not want do explode anything """
pass
@classmethod
def state(cls, widget):
"""Return the widget state"""
if widget.name == '{}.rx'.format(WIDGET_NAME):
return 'rx'
elif widget.name == '{}.tx'.format(WIDGET_NAME):
return 'tx'
return None
def update(self, widgets):
try:
bytes_recv = self._bandwidth.bytes_recv()
bytes_sent = self._bandwidth.bytes_sent()
download_rate = (bytes_recv - self._bytes_recv)
upload_rate = (bytes_sent - self._bytes_sent)
self.update_widgets(widgets, download_rate, upload_rate)
self._bytes_recv, self._bytes_sent = bytes_recv, bytes_sent
except Exception:
""" We do not want do explode anything """
pass
@classmethod
def update_widgets(cls, widgets, download_rate, upload_rate):
"""Update tx/rx widgets with new rates"""
del widgets[:]
widgets.extend((
TrafficWidget(text=download_rate, direction='rx'),
TrafficWidget(text=upload_rate, direction='tx')
))
class BandwidthInfo(object):
"""Get received/sent bytes from network adapter"""
def bytes_recv(self):
"""Return received bytes"""
return self.bandwidth().bytes_recv
def bytes_sent(self):
"""Return sent bytes"""
return self.bandwidth().bytes_sent
def bandwidth(self):
"""Return bandwidth information"""
io_counters = self.io_counters()
return io_counters[self.default_network_adapter()]
@classmethod
def default_network_adapter(cls):
"""Return default active network adapter"""
gateway = netifaces.gateways()['default']
if not gateway:
raise 'No default gateway found'
return gateway[netifaces.AF_INET][1]
@classmethod
def io_counters(cls):
"""Return IO counters"""
return psutil.net_io_counters(pernic=True)
class TrafficWidget(object):
"""Create a traffic widget with humanized bytes string with proper icon (up/down)"""
def __new__(cls, text, direction):
widget = bumblebee.output.Widget(name='{0}.{1}'.format(WIDGET_NAME, direction))
widget.set('theme.minwidth', '0000000KiB/s')
widget.full_text(cls.humanize(text))
return widget
@staticmethod
def humanize(text):
"""Return humanized bytes"""
humanized_byte_format = bumblebee.util.bytefmt(text)
return '{0}/s'.format(humanized_byte_format)

View file

@ -1,68 +1,68 @@
# pylint: disable=C0111,R0903 # pylint: disable=C0111,R0903
"""Displays the pi-hole status (up/down) together with the number of ads that were blocked today """Displays the pi-hole status (up/down) together with the number of ads that were blocked today
Parameters: Parameters:
* pihole.address : pi-hole address (e.q: http://192.168.1.3) * pihole.address : pi-hole address (e.q: http://192.168.1.3)
* pihole.pwhash : pi-hole webinterface password hash (can be obtained from the /etc/pihole/SetupVars.conf file) * pihole.pwhash : pi-hole webinterface password hash (can be obtained from the /etc/pihole/SetupVars.conf file)
""" """
import bumblebee.input import bumblebee.input
import bumblebee.output import bumblebee.output
import bumblebee.engine import bumblebee.engine
import requests import requests
class Module(bumblebee.engine.Module): class Module(bumblebee.engine.Module):
def __init__(self, engine, config): def __init__(self, engine, config):
super(Module, self).__init__(engine, config, super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.pihole_status) bumblebee.output.Widget(full_text=self.pihole_status)
) )
buttons = {"LEFT_CLICK":bumblebee.input.LEFT_MOUSE} buttons = {"LEFT_CLICK":bumblebee.input.LEFT_MOUSE}
self._pihole_address = self.parameter("address", "") self._pihole_address = self.parameter("address", "")
self._pihole_pw_hash = self.parameter("pwhash", "") self._pihole_pw_hash = self.parameter("pwhash", "")
self._pihole_status = None self._pihole_status = None
self._ads_blocked_today = "-" self._ads_blocked_today = "-"
self.update_pihole_status() self.update_pihole_status()
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE, engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE,
cmd=self.toggle_pihole_status) cmd=self.toggle_pihole_status)
def pihole_status(self, widget): def pihole_status(self, widget):
if self._pihole_status is None: if self._pihole_status is None:
return "pi-hole unknown" return "pi-hole unknown"
return "pi-hole " + ("up/" + self._ads_blocked_today if self._pihole_status else "down") return "pi-hole " + ("up/" + self._ads_blocked_today if self._pihole_status else "down")
def update_pihole_status(self): def update_pihole_status(self):
try: try:
data = requests.get(self._pihole_address + "/admin/api.php?summary").json() data = requests.get(self._pihole_address + "/admin/api.php?summary").json()
self._pihole_status = True if data["status"] == "enabled" else False self._pihole_status = True if data["status"] == "enabled" else False
self._ads_blocked_today = data["ads_blocked_today"] self._ads_blocked_today = data["ads_blocked_today"]
except: except:
self._pihole_status = None self._pihole_status = None
def toggle_pihole_status(self, widget): def toggle_pihole_status(self, widget):
if self._pihole_status is not None: if self._pihole_status is not None:
try: try:
req = None req = None
if self._pihole_status: if self._pihole_status:
req = requests.get(self._pihole_address + "/admin/api.php?disable&auth=" + self._pihole_pw_hash) req = requests.get(self._pihole_address + "/admin/api.php?disable&auth=" + self._pihole_pw_hash)
else: else:
req = requests.get(self._pihole_address + "/admin/api.php?enable&auth=" + self._pihole_pw_hash) req = requests.get(self._pihole_address + "/admin/api.php?enable&auth=" + self._pihole_pw_hash)
if req is not None: if req is not None:
if req.status_code == 200: if req.status_code == 200:
status = req.json()["status"] status = req.json()["status"]
self._pihole_status = False if status == "disabled" else True self._pihole_status = False if status == "disabled" else True
except: except:
pass pass
def update(self, widgets): def update(self, widgets):
self.update_pihole_status() self.update_pihole_status()
def state(self, widget): def state(self, widget):
if self._pihole_status is None: if self._pihole_status is None:
return [] return []
elif self._pihole_status: elif self._pihole_status:
return ["enabled"] return ["enabled"]
return ["disabled", "warning"] return ["disabled", "warning"]

View file

@ -4,21 +4,33 @@
Requires the following executable: Requires the following executable:
* redshift * redshift
Parameters:
* redshift.location : location provider, either of "geoclue2" (default), \
"ipinfo" (requires the requests package), or "manual"
* redshift.lat : latitude if location is set to "manual"
* redshift.lon : longitude if location is set to "manual"
""" """
import threading import threading
try:
import requests
except ImportError:
pass
import bumblebee.input import bumblebee.input
import bumblebee.output import bumblebee.output
import bumblebee.engine import bumblebee.engine
def is_terminated(): def is_terminated():
for thread in threading.enumerate(): for thread in threading.enumerate():
if thread.name == "MainThread" and not thread.is_alive(): if thread.name == "MainThread" and not thread.is_alive():
return True return True
return False return False
def get_redshift_value(widget):
def get_redshift_value(widget, location, lat, lon):
while True: while True:
if is_terminated(): if is_terminated():
return return
@ -31,8 +43,14 @@ def get_redshift_value(widget):
break break
widget.get("condition").release() widget.get("condition").release()
command = ["redshift", "-p", "-l"]
if location == "manual":
command.append(lat + ":" + lon)
else:
command.append("geoclue2")
try: try:
res = bumblebee.util.execute("redshift -p") res = bumblebee.util.execute(" ".join(command))
except Exception: except Exception:
res = "" res = ""
widget.set("temp", "n/a") widget.set("temp", "n/a")
@ -52,14 +70,40 @@ def get_redshift_value(widget):
widget.set("state", "transition") widget.set("state", "transition")
widget.set("transition", " ".join(line.split(" ")[2:])) widget.set("transition", " ".join(line.split(" ")[2:]))
class Module(bumblebee.engine.Module): class Module(bumblebee.engine.Module):
def __init__(self, engine, config): def __init__(self, engine, config):
widget = bumblebee.output.Widget(full_text=self.text) widget = bumblebee.output.Widget(full_text=self.text)
super(Module, self).__init__(engine, config, widget) super(Module, self).__init__(engine, config, widget)
self._location = self.parameter("location", "geoclue2")
self._lat = self.parameter("lat", None)
self._lon = self.parameter("lon", None)
# Even if location method is set to manual, if we have no lat or lon,
# fall back to the geoclue2 method.
if self._location == "manual" and (self._lat is None
or self._lon is None):
self._location == "geoclue2"
if self._location == "ipinfo":
try:
location_url = "http://ipinfo.io/json"
location = requests.get(location_url).json()
self._lat, self._lon = location["loc"].split(",")
self._lat = str(float(self._lat))
self._lon = str(float(self._lon))
self._location = "manual"
except Exception:
# Fall back to geoclue2.
self._location = "geoclue2"
self._text = "" self._text = ""
self._condition = threading.Condition() self._condition = threading.Condition()
widget.set("condition", self._condition) widget.set("condition", self._condition)
self._thread = threading.Thread(target=get_redshift_value, args=(widget,)) self._thread = threading.Thread(target=get_redshift_value,
args=(widget, self._location,
self._lat, self._lon))
self._thread.start() self._thread.start()
self._condition.acquire() self._condition.acquire()
self._condition.notify() self._condition.notify()

311
bumblebee/modules/rss.py Normal file
View file

@ -0,0 +1,311 @@
# pylint: disable=C0111,R0903
"""RSS news ticker
Fetches rss news items and shows these as a news ticker.
Left-clicking will open the full story in a browser.
New stories are highlighted.
Parameters:
* rss.feeds : Space-separated list of RSS URLs
* rss.length : Maximum length of the module, default is 60
"""
try:
import feedparser
DEPENDENCIES_OK = True
except ImportError:
DEPENDENCIES_OK = False
import webbrowser
import time
import os
import tempfile
import logging
import random
import re
import json
import bumblebee.input
import bumblebee.output
import bumblebee.engine
# pylint: disable=too-many-instance-attributes
class Module(bumblebee.engine.Module):
REFRESH_DELAY = 600
SCROLL_SPEED = 3
LAYOUT_STYLES_ITEMS = [[1,1,1],[3,3,2],[2,3,3],[3,2,3]]
HISTORY_FILENAME = ".config/i3/rss.hist"
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.ticker_update if DEPENDENCIES_OK else self._show_error)
)
# Use BBC newsfeed as demo:
self._feeds = self.parameter('feeds', 'https://www.espn.com/espn/rss/news').split(" ")
self._feeds_to_update = []
self._max_title_length = int(self.parameter("length", 60))
self._items = []
self._current_item = None
self._ticker_offset = 0
self._pre_delay = 0
self._post_delay = 0
self._state = []
self._newspaper_filename = tempfile.mktemp('.html')
self._last_refresh = 0
self._last_update = 0
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE, cmd=self._open)
engine.input.register_callback(self, button=bumblebee.input.RIGHT_MOUSE, cmd=self._create_newspaper)
self._history = {'ticker': {}, 'newspaper': {}}
self._load_history()
def _load_history(self):
if os.path.isfile(self.HISTORY_FILENAME):
self._history = json.loads(open(self.HISTORY_FILENAME, "r").read())
def _update_history(self, group):
sources = set([i['source'] for i in self._items])
self._history[group] = dict([[s, [i['title'] for i in self._items if i['source'] == s]] for s in sources])
def _save_history(self):
if not os.path.exists(os.path.dirname(self.HISTORY_FILENAME)):
os.makedirs(os.path.dirname(self.HISTORY_FILENAME))
open(self.HISTORY_FILENAME, "w").write(json.dumps(self._history))
def _check_history(self, items, group):
for i in items:
i['new'] = not (i['source'] in self._history[group] and i['title'] in self._history[group][i['source']])
def _open(self, _):
if self._current_item:
webbrowser.open(self._current_item['link'])
def _check_for_image(self, entry):
image = next(iter([l['href'] for l in entry['links'] if l['rel'] == 'enclosure']), None)
if not image and 'media_content' in entry:
try:
media = sorted(entry['media_content'], key=lambda i: i['height'] if 'height' in i else 0, reverse=True)
image = next(iter([i['url'] for i in media if i['medium'] == 'image']), None)
except Exception:
pass
if not image:
match = re.search(r'<img[^>]*src\s*=["\']*([^\s^>^"^\']*)["\']*', entry['summary'])
if match:
image = match.group(1)
return image if image else ''
def _remove_tags(self, txt):
return re.sub('<[^>]*>', '', txt)
def _create_item(self, entry, url, feed):
return {'title': self._remove_tags(entry['title'].replace('\n', ' ')),
'link': entry['link'],
'new': True,
'source': url,
'summary': self._remove_tags(entry['summary']),
'feed': feed,
'image': self._check_for_image(entry),
'published': time.mktime(entry.published_parsed) if hasattr(entry, 'published_parsed') else 0}
def _update_items_from_feed(self, url):
parser = feedparser.parse(url)
new_items = [self._create_item(entry, url, parser['feed']['title']) for entry in parser['entries']]
# Check history
self._check_history(new_items, 'ticker')
# Remove the previous items
self._items = [i for i in self._items if i['source'] != url]
# Add the new items
self._items.extend(new_items)
# Sort the items on publish date
self._items.sort(key=lambda i: i['published'], reverse=True)
def _check_for_refresh(self):
if self._feeds_to_update:
# Update one feed at a time to not overload this update cycle
url = self._feeds_to_update.pop()
self._update_items_from_feed(url)
if not self._feeds_to_update:
self._update_history('ticker')
self._save_history()
if not self._current_item:
self._next_item()
elif time.time()-self._last_refresh >= self.REFRESH_DELAY:
# Populate the list with feeds to update
self._feeds_to_update = self._feeds[:]
# Update the refresh time
self._last_refresh = time.time()
def _next_item(self):
self._ticker_offset = 0
self._pre_delay = 2
self._post_delay = 4
if not self._items:
return
# Index of the current element
idx = self._items.index(self._current_item) if self._current_item in self._items else - 1
# First show new items, else show next
new_items = [i for i in self._items if i['new']]
self._current_item = next(iter(new_items), self._items[(idx+1) % len(self._items)])
def _check_scroll_done(self):
# Check if the complete title has been shown
if self._ticker_offset + self._max_title_length > len(self._current_item['title']):
# Do not immediately show next item after scroll
self._post_delay -= 1
if self._post_delay == 0:
self._current_item['new'] = False
# Mark the previous item as 'old'
self._next_item()
else:
# Increase scroll position
self._ticker_offset += self.SCROLL_SPEED
def _show_error(self, _):
return "Please install feedparser first"
def ticker_update(self, _):
# Only update the ticker once a second
now = time.time()
if now-self._last_update < 1:
return self._response
self._last_update = now
self._check_for_refresh()
# If no items were retrieved, return an empty string
if not self._current_item:
return " "*self._max_title_length
# Prepare a substring of the item title
self._response = self._current_item['title'][self._ticker_offset:self._ticker_offset+self._max_title_length]
# Add spaces if too short
self._response = self._response.ljust(self._max_title_length)
# Do not immediately scroll
if self._pre_delay > 0:
# Change state during pre_delay for new items
if self._current_item['new']:
self._state = ['warning']
self._pre_delay -= 1
return self._response
self._state = []
self._check_scroll_done()
return self._response
def update(self, widgets):
pass
def state(self, _):
return self._state
def _create_news_element(self, item, overlay_title):
try:
timestr = "" if item['published'] == 0 else str(time.ctime(item['published']))
except Exception as exc:
logging.error(str(exc))
raise e
element = "<div class='item' onclick=window.open('"+item['link']+"')>"
element += "<div class='titlecontainer'>"
element += " <img "+("" if item['image'] else "class='noimg' ")+"src='"+item['image']+"'>"
element += " <div class='title"+(" overlay" if overlay_title else "")+"'>"+("<span class='star'>&#x2605;</span>" if item['new'] else "")+item['title']+"</div>"
element += "</div>"
element += "<div class='summary'>"+item['summary']+"</div>"
element += "<div class='info'><span class='author'>"+item['feed']+"</span><span class='published'>"+timestr+"</span></div>"
element += "</div>"
return element
def _create_news_section(self, newspaper_items):
style = random.randint(0, 3)
section = "<table><tr class='style"+str(style)+"'>"
for i in range(0, 3):
section += "<td><div class='itemcontainer'>"
for _ in range(0, self.LAYOUT_STYLES_ITEMS[style][i]):
if newspaper_items:
section += self._create_news_element(newspaper_items[0], self.LAYOUT_STYLES_ITEMS[style][i] != 3)
del newspaper_items[0]
section += "</div></td>"
section += "</tr></table>"
return section
def _create_newspaper(self, _):
content = ""
newspaper_items = self._items[:]
self._check_history(newspaper_items, 'newspaper')
# Make sure new items are always listed first, independent of publish date
newspaper_items.sort(key=lambda i: i['published']+(10000000 if i['new'] else 0), reverse=True)
while newspaper_items:
content += self._create_news_section(newspaper_items)
open(self._newspaper_filename, "w").write(HTML_TEMPLATE.replace("[[CONTENT]]", content))
webbrowser.open("file://"+self._newspaper_filename)
self._update_history('newspaper')
self._save_history()
HTML_TEMPLATE = """<!DOCTYPE html>
<html>
<head>
<script>
window.onload = function() {
var images = document.getElementsByTagName('img');
// Remove very small images
for(var i = 0; i < images.length; i++) {
if (images[i].naturalWidth<50 || images[i].naturalHeight<50) {
images[i].src = ''
images[i].className+=' noimg'
}
}
}
</script>
</head>
<style>
body {background: #eee; font-family: Helvetica neue;}
td {background: #fff; height: 100%;}
tr.style0 td {width: 33%;}
tr.style1 td {width: 20%;}
tr.style1 td:last-child {width: 60%;}
tr.style2 td {width: 20%;}
tr.style2 td:first-child {width: 60%;}
tr.style3 td {width: 20%;}
tr.style3 td:nth-child(2) {width: 60%;}
img {width: 100%; display: block; }
img.noimg {min-height:250px; background: #1299c8;}
#content {width: 1500px; margin: auto; background: #eee; padding: 1px;}
#newspapertitle {text-align: center; font-size: 60px; font-family: Arial Black; background: #1299c8; font-style: Italic; padding: 10px; color: #fff; }
.star {color: #ffa515; font-size: 24px;}
.section {display: flex;}
.column {display: flex;}
.itemcontainer {width: 100%; height: 100%; position: relative; display: inline-table;}
.item {cursor: pointer; }
.titlecontainer {position: relative;}
.title.overlay {font-family: Arial; position: absolute; bottom: 10px; color: #fff; font-weight: bold; text-align: right; max-width: 75%; right: 10px; font-size: 23px; text-shadow: 1px 0 0 #000, 0 -1px 0 #000, 0 1px 0 #000, -1px 0 0 #000;}
.title:not(.overlay) {font-weight: bold; padding: 0px 10px;}
.summary {color: #444; padding: 10px 10px 0px 10px; font-family: Times new roman; font-size: 18px; flex: 1;max-height: 105px; overflow: hidden;}
.info {color: #aaa; font-family: arial; font-size: 13px; padding: 10px;}
.published {float: right;}
</style>
<body>
<div id='content'>
<div id='newspapertitle'>Bumblebee Daily</div>
[[CONTENT]]
</div>
</body>
</html>"""
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

111
bumblebee/modules/sun.py Normal file
View file

@ -0,0 +1,111 @@
# pylint: disable=C0111,R0903
"""Displays sunrise and sunset times
Parameters:
* cpu.lat : Latitude of your location
* cpu.lon : Longitude of your location
"""
try:
from suntime import Sun, SunTimeException
except ImportError:
pass
try:
import requests
except ImportError:
pass
try:
from dateutil.tz import tzlocal
except ImportError:
pass
import datetime
import bumblebee.input
import bumblebee.output
import bumblebee.engine
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(
engine, config,
bumblebee.output.Widget(full_text=self.suntimes)
)
self.interval(3600)
self._lat = self.parameter("lat", None)
self._lon = self.parameter("lon", None)
try:
if not self._lat or not self._lon:
location_url = "http://ipinfo.io/json"
location = requests.get(location_url).json()
self._lat, self._lon = location["loc"].split(",")
self._lat = float(self._lat)
self._lon = float(self._lon)
except Exception:
pass
self.update(None)
def suntimes(self, _):
if self._sunset and self._sunrise:
if self._isup:
return u"\u21A7{} \u21A5{}".format(
self._sunset.strftime('%H:%M'),
self._sunrise.strftime('%H:%M'))
return u"\u21A5{} \u21A7{}".format(self._sunrise.strftime('%H:%M'),
self._sunset.strftime('%H:%M'))
return "?"
def _calculate_times(self):
self._isup = False
try:
sun = Sun(self._lat, self._lon)
except Exception:
self._sunrise = None
self._sunset = None
return
order_matters = True
try:
self._sunrise = sun.get_local_sunrise_time()
except SunTimeException:
self._sunrise = "no sunrise"
order_matters = False
try:
self._sunset = sun.get_local_sunset_time()
except SunTimeException:
self._sunset = "no sunset"
order_matters = False
if not order_matters:
return
now = datetime.datetime.now(tz=tzlocal())
if now > self._sunset:
tomorrow = (now + datetime.timedelta(days=1)).date()
try:
self._sunrise = sun.get_local_sunrise_time(tomorrow)
self._sunset = sun.get_local_sunset_time(tomorrow)
except SunTimeException:
self._sunrise = "no sunrise"
self._sunset = "no sunset"
elif now > self._sunrise:
tomorrow = (now + datetime.timedelta(days=1)).date()
try:
self._sunrise = sun.get_local_sunrise_time(tomorrow)
except SunTimeException:
self._sunrise = "no sunrise"
return
self._isup = True
def update(self, widgets):
if not self._lat or not self._lon:
self._sunrise = None
self._sunset = None
self._calculate_times()
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
# pylint: disable=C0111,R0903
""" system module
adds the possibility to
* shutdown
* reboot
the system.
Per default a confirmation dialog is shown before the actual action is performed.
Paramters:
* system.confirm: show confirmation dialog before performing any action (default: true)
"""
import logging
import bumblebee.input
import bumblebee.output
import bumblebee.engine
import bumblebee.popup_v2
import functools
try:
import Tkinter as tk
import tkMessageBox as tkmessagebox
except ImportError:
# python 3
try:
import tkinter as tk
from tkinter import messagebox as tkmessagebox
except ImportError:
logging.warning("failed to import tkinter - bumblebee popups won't work!")
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.text)
)
self._confirm = True
if self.parameter("confirm", "true") == "false":
self._confirm = False
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE,
cmd=self.popup)
def update(self, widgets):
pass
def text(self, widget):
return ""
def _on_command(self, header, text, command):
do_it = True
if self._confirm:
root = tk.Tk()
root.withdraw()
root.focus_set()
do_it = tkmessagebox.askyesno(header, text)
root.destroy()
if do_it:
bumblebee.util.execute(command)
def popup(self, widget):
menu = bumblebee.popup_v2.PopupMenu()
menu.add_menuitem("shutdown", callback=functools.partial(self._on_command, "Shutdown", "Shutdown?", "shutdown -h now"))
menu.add_menuitem("reboot", callback=functools.partial(self._on_command, "Reboot", "Reboot?", "reboot"))
menu.add_menuitem("log out", callback=functools.partial(self._on_command, "Log out", "Log out?", "i3exit logout"))
# don't ask for these
menu.add_menuitem("switch user", callback=functools.partial(bumblebee.util.execute, "i3exit switch_user"))
menu.add_menuitem("lock", callback=functools.partial(bumblebee.util.execute, "i3exit lock"))
menu.add_menuitem("suspend", callback=functools.partial(bumblebee.util.execute, "i3exit suspend"))
menu.add_menuitem("hibernate", callback=functools.partial(bumblebee.util.execute, "i3exit hibernate"))
menu.show(widget)
def state(self, widget):
return []

View file

@ -103,7 +103,8 @@ class Module(bumblebee.engine.Module):
widget = self.create_widget(widgets, name, attributes={"theme.minwidth": "1000.00MB"}) widget = self.create_widget(widgets, name, attributes={"theme.minwidth": "1000.00MB"})
prev = self._prev.get(name, 0) prev = self._prev.get(name, 0)
speed = bumblebee.util.bytefmt((int(data[direction]) - int(prev))/timediff) speed = bumblebee.util.bytefmt((int(data[direction]) - int(prev))/timediff)
widget.full_text(speed) txtspeed ='{0}/s'.format(speed)
widget.full_text(txtspeed)
self._prev[name] = data[direction] self._prev[name] = data[direction]
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

39
bumblebee/modules/twmn.py Normal file
View file

@ -0,0 +1,39 @@
#pylint: disable=C0111,R0903
"""Toggle twmn notifications."""
import bumblebee.input
import bumblebee.output
import bumblebee.engine
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text="")
)
self._paused = False
# Make sure that twmn is currently not paused
try:
bumblebee.util.execute("killall -SIGUSR2 twmnd")
except:
pass
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE,
cmd=self.toggle_status
)
def toggle_status(self, event):
self._paused = not self._paused
try:
if self._paused:
bumblebee.util.execute("systemctl --user start twmnd")
else:
bumblebee.util.execute("systemctl --user stop twmnd")
except:
self._paused = not self._paused # toggling failed
def state(self, widget):
if self._paused:
return ["muted"]
return ["unmuted"]

View file

@ -0,0 +1,81 @@
# pylint: disable=C0111,R0903
"""Copy passwords from a password store into the clipboard (currently supports only "pass")
Many thanks to [@bbernhard](https://github.com/bbernhard) for the idea!
Parameters:
* vault.duration: Duration until password is cleared from clipboard (defaults to 30)
* vault.location: Location of the password store (defaults to ~/.password-store)
* vault.offx: x-axis offset of popup menu (defaults to 0)
* vault.offy: y-axis offset of popup menu (defaults to 0)
"""
# TODO:
# - support multiple backends by abstracting the menu structure into a tree
# - build the menu and the actions based on that abstracted tree
#
import os
import time
import threading
import bumblebee.util
import bumblebee.popup_v2
import bumblebee.input
import bumblebee.output
import bumblebee.engine
def build_menu(parent, current_directory, callback):
with os.scandir(current_directory) as it:
for entry in it:
if entry.name.startswith("."): continue
if entry.is_file():
name = entry.name[:entry.name.rfind(".")]
parent.add_menuitem(name, callback=lambda : callback(os.path.join(current_directory, name)))
else:
submenu = bumblebee.popup_v2.PopupMenu(parent, leave=False)
build_menu(submenu, os.path.join(current_directory, entry.name), callback)
parent.add_cascade(entry.name, submenu)
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.text)
)
self._duration = int(self.parameter("duration", 30))
self._offx = int(self.parameter("offx", 0))
self._offy = int(self.parameter("offy", 0))
self._path = os.path.expanduser(self.parameter("location", "~/.password-store/"))
self._reset()
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE,
cmd=self.popup)
def popup(self, widget):
menu = bumblebee.popup_v2.PopupMenu(leave=False)
build_menu(menu, self._path, self._callback)
menu.show(widget, offset_x=self._offx, offset_y=self._offy)
def _reset(self):
self._timer = None
self._text = "<click-for-password>"
def _callback(self, secret_name):
secret_name = secret_name.replace(self._path, "") # remove common path
if self._timer:
self._timer.cancel()
# bumblebee.util.execute hangs for some reason
os.system("PASSWORD_STORE_CLIP_TIME={} pass -c {} > /dev/null 2>&1".format(self._duration, secret_name))
self._timer = threading.Timer(self._duration, self._reset)
self._timer.start()
self._start = int(time.time())
self._text = secret_name
def text(self, widget):
if self._timer:
return "{} ({}s)".format(self._text, self._duration - (int(time.time()) - self._start))
return self._text
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,95 +1,95 @@
# pylint: disable=C0111,R0903 # pylint: disable=C0111,R0903
""" Displays the VPN profile that is currently in use. """ Displays the VPN profile that is currently in use.
Left click opens a popup menu that lists all available VPN profiles and allows to establish Left click opens a popup menu that lists all available VPN profiles and allows to establish
a VPN connection using that profile. a VPN connection using that profile.
Prerequisites: Prerequisites:
* nmcli needs to be installed and configured properly. * nmcli needs to be installed and configured properly.
To quickly test, whether nmcli is working correctly, type "nmcli -g NAME,TYPE,DEVICE con" which To quickly test, whether nmcli is working correctly, type "nmcli -g NAME,TYPE,DEVICE con" which
lists all the connection profiles that are configured. Make sure that your VPN profile is in that list! lists all the connection profiles that are configured. Make sure that your VPN profile is in that list!
e.g: to import a openvpn profile via nmcli: e.g: to import a openvpn profile via nmcli:
sudo nmcli connection import type openvpn file </path/to/your/openvpn/profile.ovpn> sudo nmcli connection import type openvpn file </path/to/your/openvpn/profile.ovpn>
""" """
import logging import logging
import bumblebee.input import bumblebee.input
import bumblebee.output import bumblebee.output
import bumblebee.engine import bumblebee.engine
import functools import functools
class Module(bumblebee.engine.Module): class Module(bumblebee.engine.Module):
def __init__(self, engine, config): def __init__(self, engine, config):
super(Module, self).__init__(engine, config, super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.vpn_status) bumblebee.output.Widget(full_text=self.vpn_status)
) )
self._connected_vpn_profile = None self._connected_vpn_profile = None
self._selected_vpn_profile = None self._selected_vpn_profile = None
res = bumblebee.util.execute("nmcli -g NAME,TYPE c") res = bumblebee.util.execute("nmcli -g NAME,TYPE c")
lines = res.splitlines() lines = res.splitlines()
self._vpn_profiles = [] self._vpn_profiles = []
for line in lines: for line in lines:
info = line.split(':') info = line.split(':')
try: try:
if info[1] == "vpn": if info[1] == "vpn":
self._vpn_profiles.append(info[0]) self._vpn_profiles.append(info[0])
except: except:
pass pass
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE, engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE,
cmd=self.popup) cmd=self.popup)
def update(self, widgets): def update(self, widgets):
try: try:
res = bumblebee.util.execute("nmcli -g NAME,TYPE,DEVICE con") res = bumblebee.util.execute("nmcli -g NAME,TYPE,DEVICE con")
lines = res.splitlines() lines = res.splitlines()
self._connected_vpn_profile = None self._connected_vpn_profile = None
for line in lines: for line in lines:
info = line.split(':') info = line.split(':')
if info[1] == "vpn" and info[2] != "": if info[1] == "vpn" and info[2] != "":
self._connected_vpn_profile = info[0] self._connected_vpn_profile = info[0]
except Exception as e: except Exception as e:
logging.exception("Couldn't get VPN status") logging.exception("Couldn't get VPN status")
self._connected_vpn_profile = None self._connected_vpn_profile = None
def vpn_status(self, widget): def vpn_status(self, widget):
if self._connected_vpn_profile is None: if self._connected_vpn_profile is None:
return "off" return "off"
return self._connected_vpn_profile return self._connected_vpn_profile
def _on_vpn_disconnect(self): def _on_vpn_disconnect(self):
try: try:
bumblebee.util.execute("nmcli c down " + self._connected_vpn_profile) bumblebee.util.execute("nmcli c down " + self._connected_vpn_profile)
self._connected_vpn_profile = None self._connected_vpn_profile = None
except Exception as e: except Exception as e:
logging.exception("Couldn't disconnect VPN connection") logging.exception("Couldn't disconnect VPN connection")
def _on_vpn_connect(self, name): def _on_vpn_connect(self, name):
self._selected_vpn_profile = name self._selected_vpn_profile = name
try: try:
bumblebee.util.execute("nmcli c up " + self._selected_vpn_profile) bumblebee.util.execute("nmcli c up " + self._selected_vpn_profile)
self._connected_vpn_profile = name self._connected_vpn_profile = name
except Exception as e: except Exception as e:
logging.exception("Couldn't establish VPN connection") logging.exception("Couldn't establish VPN connection")
self._connected_vpn_profile = None self._connected_vpn_profile = None
def popup(self, widget): def popup(self, widget):
menu = bumblebee.popup_v2.PopupMenu() menu = bumblebee.popup_v2.PopupMenu()
if self._connected_vpn_profile is not None: if self._connected_vpn_profile is not None:
menu.add_menuitem("Disconnect", callback=self._on_vpn_disconnect) menu.add_menuitem("Disconnect", callback=self._on_vpn_disconnect)
for vpn_profile in self._vpn_profiles: for vpn_profile in self._vpn_profiles:
if self._connected_vpn_profile is not None and self._connected_vpn_profile == vpn_profile: if self._connected_vpn_profile is not None and self._connected_vpn_profile == vpn_profile:
continue continue
menu.add_menuitem(vpn_profile, callback=functools.partial(self._on_vpn_connect, vpn_profile)) menu.add_menuitem(vpn_profile, callback=functools.partial(self._on_vpn_connect, vpn_profile))
menu.show(widget) menu.show(widget)
def state(self, widget): def state(self, widget):
return [] return []

View file

@ -118,8 +118,6 @@ class Module(bumblebee.engine.Module):
self._temperature = int(weather['main']['temp']) self._temperature = int(weather['main']['temp'])
self._weather = weather['weather'][0]['main'].lower() self._weather = weather['weather'][0]['main'].lower()
self._valid = True self._valid = True
except RequestException:
self._valid = False
except Exception: except Exception:
self._valid = False self._valid = False

View file

@ -13,13 +13,27 @@ except ImportError:
import functools import functools
class PopupMenu(object): class PopupMenu(object):
def __init__(self): def __init__(self, parent=None, leave=True):
self._root = tk.Tk()
self._root.withdraw() if not parent:
self._menu = tk.Menu(self._root) self._root = tk.Tk()
self._menu.bind("<FocusOut>", self._on_focus_out) self._root.withdraw()
self._menu = tk.Menu(self._root, tearoff=0)
self._menu.bind("<FocusOut>", self._on_focus_out)
else:
self._root = parent.root()
self._root.withdraw()
self._menu = tk.Menu(self._root, tearoff=0)
self._menu.bind("<FocusOut>", self._on_focus_out)
if leave:
self._menu.bind("<Leave>", self._on_focus_out)
def root(self):
return self._root
def menu(self):
return self._menu
def _on_focus_out(self, event=None): def _on_focus_out(self, event=None):
self._root.destroy() self._root.destroy()
@ -28,13 +42,15 @@ class PopupMenu(object):
self._root.destroy() self._root.destroy()
callback() callback()
def add_cascade(self, menuitem, submenu):
self._menu.add_cascade(label=menuitem, menu=submenu.menu())
def add_menuitem(self, menuitem, callback): def add_menuitem(self, menuitem, callback):
self._menu.add_command(label=menuitem, command=functools.partial(self._on_click, callback)) self._menu.add_command(label=menuitem, command=functools.partial(self._on_click, callback))
def show(self, event): def show(self, event, offset_x=0, offset_y=0):
try: try:
self._menu.tk_popup(event['x'], event['y']) self._menu.tk_popup(event['x'] + offset_x, event['y'] + offset_y)
finally: finally:
self._menu.grab_release() self._menu.grab_release()
self._root.mainloop() self._root.mainloop()

View file

@ -105,6 +105,9 @@ class Theme(object):
if icon is None: if icon is None:
return self._get(widget, "prefix", None) return self._get(widget, "prefix", None)
def get(self, widget, attribute, default_value=""):
return self._get(widget, attribute, default_value)
def padding(self, widget): def padding(self, widget):
"""Return padding for widget""" """Return padding for widget"""
return self._get(widget, "padding", "") return self._get(widget, "padding", "")
@ -223,6 +226,9 @@ class Theme(object):
if not self._widget: if not self._widget:
self._widget = widget self._widget = widget
if self._widget.get("theme.exclude", "") == name:
return None
if self._widget != widget: if self._widget != widget:
self._prevbg = self.bg(self._widget) self._prevbg = self.bg(self._widget)
self._widget = widget self._widget = widget
@ -238,7 +244,8 @@ class Theme(object):
states = widget.state() states = widget.state()
if name not in states: if name not in states:
for state in states: for state in states:
state_themes.append(self._get(widget, state, {})) if state:
state_themes.append(self._get(widget, state, {}))
value = self._defaults.get(name, default) value = self._defaults.get(name, default)
value = widget.get("theme.{}".format(name), value) value = widget.get("theme.{}".format(name), value)

BIN
screenshots/http_status.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
screenshots/vault.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -14,7 +14,7 @@ def rand(cnt):
return "".join(random.choice("abcdefghijklmnopqrstuvwxyz0123456789") for i in range(cnt)) return "".join(random.choice("abcdefghijklmnopqrstuvwxyz0123456789") for i in range(cnt))
def setup_test(test, Module): def setup_test(test, Module):
test._stdin, test._select, test.stdin, test.select = epoll_mock("bumblebee.input") test._stdin, test._select, test.stdin, test.select = poll_mock("bumblebee.input")
test.popen = MockPopen() test.popen = MockPopen()
@ -33,19 +33,19 @@ def teardown_test(test):
test._select.stop() test._select.stop()
test.popen.cleanup() test.popen.cleanup()
def epoll_mock(module=""): def poll_mock(module=""):
if len(module) > 0: module = "{}.".format(module) if len(module) > 0: module = "{}.".format(module)
stdin = mock.patch("{}sys.stdin".format(module)) stdin = mock.patch("{}sys.stdin".format(module))
select = mock.patch("{}select".format(module)) select = mock.patch("{}select".format(module))
epoll = mock.Mock() poll = mock.Mock()
stdin_mock = stdin.start() stdin_mock = stdin.start()
select_mock = select.start() select_mock = select.start()
stdin_mock.fileno.return_value = 1 stdin_mock.fileno.return_value = 1
select_mock.epoll.return_value = epoll select_mock.poll.return_value = poll
epoll.poll.return_value = [(stdin_mock.fileno.return_value, 100)] poll.poll.return_value = [(stdin_mock.fileno.return_value, 100)]
return stdin, select, stdin_mock, select_mock return stdin, select, stdin_mock, select_mock

View file

@ -0,0 +1,49 @@
# pylint: disable=C0103,C0111
import mock
import unittest
from bumblebee.modules.http_status import Module
from bumblebee.config import Config
class TestHttpStatusModule(unittest.TestCase):
def test_status_success(self):
config = Config()
config.set("http_status.target", "http://example.org")
self.module = Module(engine=mock.Mock(), config={"config":config})
self.assertTrue(not "warning" in self.module.state(self.module.widgets()[0]))
self.assertTrue(not "critical" in self.module.state(self.module.widgets()[0]))
self.assertEqual(self.module.getStatus(), "200")
self.assertEqual(self.module.getOutput(), "200")
def test_status_error(self):
config = Config()
config.set("http_status.expect", "not a 200")
config.set("http_status.target", "http://example.org")
self.module = Module(engine=mock.Mock(), config={"config":config})
self.assertTrue(not "warning" in self.module.state(self.module.widgets()[0]))
self.assertTrue("critical" in self.module.state(self.module.widgets()[0]))
self.assertEqual(self.module.getStatus(), "200")
self.assertEqual(self.module.getOutput(), "200 != not a 200")
def test_label(self):
config = Config()
config.set("http_status.label", "example")
config.set("http_status.target", "http://example.org")
self.module = Module(engine=mock.Mock(), config={"config":config})
self.assertEqual(self.module.getOutput(), "example: 200")
def test_unknow(self):
config = Config()
config.set("http_status.target", "invalid target")
self.module = Module(engine=mock.Mock(), config={"config":config})
self.assertTrue("warning" in self.module.state(self.module.widgets()[0]))
self.assertEqual(self.module.getStatus(), "UNK")
self.assertEqual(self.module.getOutput(), "UNK != 200")
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -20,10 +20,10 @@ class TestI3BarInput(unittest.TestCase):
self.popen = mocks.MockPopen() self.popen = mocks.MockPopen()
self.stdin.fileno.return_value = 1 self.stdin.fileno.return_value = 1
epoll = mock.Mock() poll = mock.Mock()
self.select.epoll.return_value = epoll self.select.poll.return_value = poll
epoll.poll.return_value = [(self.stdin.fileno.return_value, 2)] poll.poll.return_value = [(self.stdin.fileno.return_value, 2)]
self.anyModule = mock.Mock() self.anyModule = mock.Mock()
self.anyModule.id = mocks.rand(10) self.anyModule.id = mocks.rand(10)

View file

@ -118,6 +118,14 @@ class TestTheme(unittest.TestCase):
# widget theme instead (i.e. no fallback to a more general state theme) # widget theme instead (i.e. no fallback to a more general state theme)
self.assertEquals(theme.bg(self.themedWidget), data[self.widgetTheme]["bg"]) self.assertEquals(theme.bg(self.themedWidget), data[self.widgetTheme]["bg"])
def test_empty_state(self):
theme = self.theme
data = theme.data()
self.anyModule.state.return_value = ""
self.assertEquals(theme.fg(self.anyWidget), data["defaults"]["fg"])
self.assertEquals(theme.bg(self.anyWidget), data["defaults"]["bg"])
def test_separator(self): def test_separator(self):
self.assertEquals(self.validThemeSeparator, self.theme.separator(self.anyWidget)) self.assertEquals(self.validThemeSeparator, self.theme.separator(self.anyWidget))

View file

@ -1,133 +1,307 @@
{ {
"defaults": { "defaults": {
"padding": " " "padding": " "
}, },
"memory": { "prefix": "ram" }, "memory": {
"cpu": { "prefix": "cpu" }, "prefix": "ram"
"disk": { "prefix": "hdd" }, },
"dnf": { "prefix": "dnf" }, "cpu": {
"apt": { "prefix": "apt" }, "prefix": "cpu"
"brightness": { "prefix": "o" }, },
"cmus": { "disk": {
"playing": { "prefix": ">" }, "prefix": "hdd"
"paused": { "prefix": "||" }, },
"stopped": { "prefix": "[]" }, "dnf": {
"prev": { "prefix": "|<" }, "prefix": "dnf"
"next": { "prefix": ">|" }, },
"shuffle-on": { "prefix": "S" }, "apt": {
"shuffle-off": { "prefix": "[s]" }, "prefix": "apt"
"repeat-on": { "prefix": "R" }, },
"repeat-off": { "prefix": "[r]" } "brightness": {
}, "prefix": "o"
"pasink": { },
"muted": { "prefix": "audio(mute)" }, "cmus": {
"unmuted": { "prefix": "audio" } "playing": {
}, "prefix": ">"
"amixer": { },
"muted": { "prefix": "audio(mute)" }, "paused": {
"unmuted": { "prefix": "audio" } "prefix": "||"
}, },
"pasource": { "stopped": {
"muted": { "prefix": "mic(mute)" }, "prefix": "[]"
"unmuted": { "prefix": "mic" } },
}, "prev": {
"nic": { "prefix": "|<"
"wireless-up": { "prefix": "wifi" }, },
"wireless-down": { "prefix": "wifi" }, "next": {
"wired-up": { "prefix": "lan" }, "prefix": ">|"
"wired-down": { "prefix": "lan" }, },
"tunnel-up": { "prefix": "tun" }, "shuffle-on": {
"tunnel-down": { "prefix": "tun" } "prefix": "S"
}, },
"battery": { "shuffle-off": {
"charged": { "suffix": "full" }, "prefix": "[s]"
"charging": { "suffix": "chr" }, },
"AC": { "suffix": "ac" }, "repeat-on": {
"discharging-10": { "prefix": "R"
"prefix": "!", },
"suffix": "dis" "repeat-off": {
}, "prefix": "[r]"
"discharging-25": { "suffix": "dis" },
"discharging-50": { "suffix": "dis" },
"discharging-80": { "suffix": "dis" },
"discharging-100": { "suffix": "dis" },
"unknown-25": { "suffix": "?" },
"unknown-50": { "suffix": "?" },
"unknown-80": { "suffix": "?" },
"unknown-100": { "suffix": "?" }
},
"battery_all": {
"charged": { "suffix": "full" },
"charging": { "suffix": "chr" },
"AC": { "suffix": "ac" },
"discharging-10": {
"prefix": "!",
"suffix": "dis"
},
"discharging-25": { "suffix": "dis" },
"discharging-50": { "suffix": "dis" },
"discharging-80": { "suffix": "dis" },
"discharging-100": { "suffix": "dis" },
"unknown-25": { "suffix": "?" },
"unknown-50": { "suffix": "?" },
"unknown-80": { "suffix": "?" },
"unknown-100": { "suffix": "?" }
},
"caffeine": {
"activated": {"prefix": "caf-on" }, "deactivated": { "prefix": "caf-off " }
},
"xrandr": {
"on": { "prefix": " off "}, "off": { "prefix": " on "}, "refresh": { "prefix": " refresh "}
},
"redshift": {
"day": { "prefix": "day" }, "night": { "prefix": "night" }, "transition": { "prefix": "trans" }
},
"docker_ps": {
"prefix": "containers"
},
"sensors": {
"prefix": "sensors"
},
"traffic": {
"rx": { "prefix": "down"},
"tx": { "prefix": "up"}
},
"mpd": {
"playing": { "prefix": ">" },
"paused": { "prefix": "||" },
"stopped": { "prefix": "[]" },
"prev": { "prefix": "|<" },
"next": { "prefix": ">|" },
"shuffle-on": { "prefix": "S" },
"shuffle-off": { "prefix": "[s]" },
"repeat-on": { "prefix": "R" },
"repeat-off": { "prefix": "[r]" }
},
"github": {
"prefix": "github"
},
"spotify": {
"prefix": ""
},
"uptime": {
"prefix": "uptime"
},
"zpool": {
"poolread": {"prefix": "pool read "},
"poolwrite": {"prefix": "pool write "},
"ONLINE": {"prefix": "pool"},
"FAULTED": {"prefix": "pool (!)"},
"DEGRADED": {"prefix": "pool (!)"}
},
"git": {
"main": { "prefix": "" },
"new": { "prefix": "[n]" },
"modified": { "prefix": "[m]" },
"deleted": { "prefix": "[d]" }
},
"dunst": {
"muted": { "prefix": "dunst(muted)"},
"unmuted": { "prefix": "dunst" }
} }
},
"pasink": {
"muted": {
"prefix": "audio(mute)"
},
"unmuted": {
"prefix": "audio"
}
},
"amixer": {
"muted": {
"prefix": "audio(mute)"
},
"unmuted": {
"prefix": "audio"
}
},
"pasource": {
"muted": {
"prefix": "mic(mute)"
},
"unmuted": {
"prefix": "mic"
}
},
"nic": {
"wireless-up": {
"prefix": "wifi"
},
"wireless-down": {
"prefix": "wifi"
},
"wired-up": {
"prefix": "lan"
},
"wired-down": {
"prefix": "lan"
},
"tunnel-up": {
"prefix": "tun"
},
"tunnel-down": {
"prefix": "tun"
}
},
"battery": {
"charged": {
"suffix": "full"
},
"charging": {
"suffix": "chr"
},
"AC": {
"suffix": "ac"
},
"discharging-10": {
"prefix": "!",
"suffix": "dis"
},
"discharging-25": {
"suffix": "dis"
},
"discharging-50": {
"suffix": "dis"
},
"discharging-80": {
"suffix": "dis"
},
"discharging-100": {
"suffix": "dis"
},
"unknown-25": {
"suffix": "?"
},
"unknown-50": {
"suffix": "?"
},
"unknown-80": {
"suffix": "?"
},
"unknown-100": {
"suffix": "?"
}
},
"battery_all": {
"charged": {
"suffix": "full"
},
"charging": {
"suffix": "chr"
},
"AC": {
"suffix": "ac"
},
"discharging-10": {
"prefix": "!",
"suffix": "dis"
},
"discharging-25": {
"suffix": "dis"
},
"discharging-50": {
"suffix": "dis"
},
"discharging-80": {
"suffix": "dis"
},
"discharging-100": {
"suffix": "dis"
},
"unknown-25": {
"suffix": "?"
},
"unknown-50": {
"suffix": "?"
},
"unknown-80": {
"suffix": "?"
},
"unknown-100": {
"suffix": "?"
}
},
"caffeine": {
"activated": {
"prefix": "caf-on"
},
"deactivated": {
"prefix": "caf-off "
}
},
"xrandr": {
"on": {
"prefix": " off "
},
"off": {
"prefix": " on "
},
"refresh": {
"prefix": " refresh "
}
},
"redshift": {
"day": {
"prefix": "day"
},
"night": {
"prefix": "night"
},
"transition": {
"prefix": "trans"
}
},
"docker_ps": {
"prefix": "containers"
},
"sensors": {
"prefix": "sensors"
},
"traffic": {
"rx": {
"prefix": "down"
},
"tx": {
"prefix": "up"
}
},
"mpd": {
"playing": {
"prefix": ">"
},
"paused": {
"prefix": "||"
},
"stopped": {
"prefix": "[]"
},
"prev": {
"prefix": "|<"
},
"next": {
"prefix": ">|"
},
"shuffle-on": {
"prefix": "S"
},
"shuffle-off": {
"prefix": "[s]"
},
"repeat-on": {
"prefix": "R"
},
"repeat-off": {
"prefix": "[r]"
}
},
"github": {
"prefix": "github"
},
"spotify": {
"prefix": ""
},
"uptime": {
"prefix": "uptime"
},
"zpool": {
"poolread": {
"prefix": "pool read "
},
"poolwrite": {
"prefix": "pool write "
},
"ONLINE": {
"prefix": "pool"
},
"FAULTED": {
"prefix": "pool (!)"
},
"DEGRADED": {
"prefix": "pool (!)"
}
},
"git": {
"main": {
"prefix": ""
},
"new": {
"prefix": "[n]"
},
"modified": {
"prefix": "[m]"
},
"deleted": {
"prefix": "[d]"
}
},
"dunst": {
"muted": {
"prefix": "dunst(muted)"
},
"unmuted": {
"prefix": "dunst"
}
},
"twmn": {
"muted": {
"prefix": "twmn"
},
"unmuted": {
"prefix": "twmn(muted)"
}
},
"system": {
"prefix": "system"
}
} }

View file

@ -1,206 +1,226 @@
{ {
"defaults": { "defaults": {
"separator": "", "padding": " ", "separator": "",
"unknown": { "prefix": "" } "padding": " ",
}, "unknown": { "prefix": "" }
"date": { "prefix": "" }, },
"time": { "prefix": "" }, "date": { "prefix": "" },
"datetime": { "prefix": "" }, "time": { "prefix": "" },
"datetz": { "prefix": "" }, "datetime": { "prefix": "" },
"timetz": { "prefix": "" }, "datetz": { "prefix": "" },
"datetimetz": { "prefix": "" }, "timetz": { "prefix": "" },
"memory": { "prefix": "" }, "datetimetz": { "prefix": "" },
"cpu": { "prefix": "" }, "memory": { "prefix": "" },
"disk": { "prefix": "" }, "cpu": { "prefix": "" },
"dnf": { "prefix": "" }, "disk": { "prefix": "" },
"apt": { "prefix": "" }, "dnf": { "prefix": "" },
"pacman": { "prefix": "" }, "apt": { "prefix": "" },
"brightness": { "prefix": "" }, "pacman": { "prefix": "" },
"load": { "prefix": "" }, "brightness": { "prefix": "" },
"layout": { "prefix": "" }, "load": { "prefix": "" },
"layout-xkb": { "prefix": "" }, "layout": { "prefix": "" },
"notmuch_count": { "empty": {"prefix": "\uf0e0" }, "layout-xkb": { "prefix": "" },
"items": {"prefix": "\uf0e0" } "notmuch_count": {
}, "empty": { "prefix": "\uf0e0" },
"todo": { "empty": {"prefix": "" }, "items": { "prefix": "\uf0e0" }
"items": {"prefix": "" }, },
"uptime": {"prefix": "" } "todo": {
}, "empty": { "prefix": "" },
"zpool": { "items": { "prefix": "" },
"poolread": {"prefix": "→ "}, "uptime": { "prefix": "" }
"poolwrite": {"prefix": "← "}, },
"ONLINE": {"prefix": ""}, "zpool": {
"FAULTED": {"prefix": "!"}, "poolread": { "prefix": "→ " },
"DEGRADED": {"prefix": "!"} "poolwrite": { "prefix": "← " },
}, "ONLINE": { "prefix": "" },
"cmus": { "FAULTED": { "prefix": "!" },
"playing": { "prefix": "" }, "DEGRADED": { "prefix": "!" }
"paused": { "prefix": "" }, },
"stopped": { "prefix": "" }, "cmus": {
"prev": { "prefix": "" }, "playing": { "prefix": "" },
"next": { "prefix": "" }, "paused": { "prefix": "" },
"shuffle-on": { "prefix": "" }, "stopped": { "prefix": "" },
"shuffle-off": { "prefix": "" }, "prev": { "prefix": "" },
"repeat-on": { "prefix": "" }, "next": { "prefix": "" },
"repeat-off": { "prefix": "" } "shuffle-on": { "prefix": "" },
}, "shuffle-off": { "prefix": "" },
"gpmdp": { "repeat-on": { "prefix": "" },
"playing": { "prefix": "" }, "repeat-off": { "prefix": "" }
"paused": { "prefix": "" }, },
"stopped": { "prefix": "" }, "gpmdp": {
"prev": { "prefix": "" }, "playing": { "prefix": "" },
"next": { "prefix": "" } "paused": { "prefix": "" },
}, "stopped": { "prefix": "" },
"pasink": { "prev": { "prefix": "" },
"muted": { "prefix": "" }, "next": { "prefix": "" }
"unmuted": { "prefix": "" } },
}, "pasink": {
"amixer": { "muted": { "prefix": "" },
"muted": { "prefix": "" }, "unmuted": { "prefix": "" }
"unmuted": { "prefix": "" } },
}, "amixer": {
"pasource": { "muted": { "prefix": "" },
"muted": { "prefix": "" }, "unmuted": { "prefix": "" }
"unmuted": { "prefix": "" } },
}, "pasource": {
"kernel": { "muted": { "prefix": "" },
"prefix": "\uf17c" "unmuted": { "prefix": "" }
},
"kernel": {
"prefix": "\uf17c"
},
"nic": {
"wireless-up": { "prefix": "" },
"wireless-down": { "prefix": "" },
"wired-up": { "prefix": "" },
"wired-down": { "prefix": "" },
"tunnel-up": { "prefix": "" },
"tunnel-down": { "prefix": "" }
},
"bluetooth": {
"ON": { "prefix": "" },
"OFF": { "prefix": "" },
"?": { "prefix": "" }
},
"battery": {
"charged": { "prefix": "", "suffix": "" },
"AC": { "suffix": "" },
"charging": {
"prefix": ["", "", "", "", ""],
"suffix": ""
}, },
"nic": { "discharging-10": { "prefix": "", "suffix": "" },
"wireless-up": { "prefix": "" }, "discharging-25": { "prefix": "", "suffix": "" },
"wireless-down": { "prefix": "" }, "discharging-50": { "prefix": "", "suffix": "" },
"wired-up": { "prefix": "" }, "discharging-80": { "prefix": "", "suffix": "" },
"wired-down": { "prefix": "" }, "discharging-100": { "prefix": "", "suffix": "" },
"tunnel-up": { "prefix": "" }, "unlimited": { "prefix": "", "suffix": "" },
"tunnel-down": { "prefix": "" } "estimate": { "prefix": "" },
}, "unknown-10": { "prefix": "", "suffix": "" },
"bluetooth": { "unknown-25": { "prefix": "", "suffix": "" },
"ON": { "prefix": "" }, "unknown-50": { "prefix": "", "suffix": "" },
"OFF": { "prefix": "" }, "unknown-80": { "prefix": "", "suffix": "" },
"?": { "prefix": "" } "unknown-100": { "prefix": "", "suffix": "" }
},
"battery_all": {
"charged": { "prefix": "", "suffix": "" },
"AC": { "suffix": "" },
"charging": {
"prefix": ["", "", "", "", ""],
"suffix": ""
}, },
"battery": { "discharging-10": { "prefix": "", "suffix": "" },
"charged": { "prefix": "", "suffix": "" }, "discharging-25": { "prefix": "", "suffix": "" },
"AC": { "suffix": "" }, "discharging-50": { "prefix": "", "suffix": "" },
"charging": { "discharging-80": { "prefix": "", "suffix": "" },
"prefix": [ "", "", "", "", "" ], "discharging-100": { "prefix": "", "suffix": "" },
"suffix": "" "unlimited": { "prefix": "", "suffix": "" },
}, "estimate": { "prefix": "" },
"discharging-10": { "prefix": "", "suffix": "" }, "unknown-10": { "prefix": "", "suffix": "" },
"discharging-25": { "prefix": "", "suffix": "" }, "unknown-25": { "prefix": "", "suffix": "" },
"discharging-50": { "prefix": "", "suffix": "" }, "unknown-50": { "prefix": "", "suffix": "" },
"discharging-80": { "prefix": "", "suffix": "" }, "unknown-80": { "prefix": "", "suffix": "" },
"discharging-100": { "prefix": "", "suffix": "" }, "unknown-100": { "prefix": "", "suffix": "" }
"unlimited": { "prefix": "", "suffix": "" }, },
"estimate": { "prefix": "" }, "caffeine": {
"unknown-10": { "prefix": "", "suffix": "" }, "activated": { "prefix": " " },
"unknown-25": { "prefix": "", "suffix": "" }, "deactivated": { "prefix": " " }
"unknown-50": { "prefix": "", "suffix": "" }, },
"unknown-80": { "prefix": "", "suffix": "" }, "xrandr": {
"unknown-100": { "prefix": "", "suffix": "" } "on": { "prefix": " " },
}, "off": { "prefix": " " },
"battery_all": { "refresh": { "prefix": "" }
"charged": { "prefix": "", "suffix": "" }, },
"AC": { "suffix": "" }, "redshift": {
"charging": { "day": { "prefix": "" },
"prefix": [ "", "", "", "", "" ], "night": { "prefix": "" },
"suffix": "" "transition": { "prefix": "" }
}, },
"discharging-10": { "prefix": "", "suffix": "" }, "docker_ps": {
"discharging-25": { "prefix": "", "suffix": "" }, "prefix": ""
"discharging-50": { "prefix": "", "suffix": "" }, },
"discharging-80": { "prefix": "", "suffix": "" }, "sensors": {
"discharging-100": { "prefix": "", "suffix": "" }, "prefix": ""
"unlimited": { "prefix": "", "suffix": "" }, },
"estimate": { "prefix": "" }, "sensors2": {
"unknown-10": { "prefix": "", "suffix": "" }, "temp": { "prefix": "" },
"unknown-25": { "prefix": "", "suffix": "" }, "fan": { "prefix": "" },
"unknown-50": { "prefix": "", "suffix": "" }, "cpu": { "prefix": "" }
"unknown-80": { "prefix": "", "suffix": "" }, },
"unknown-100": { "prefix": "", "suffix": "" } "traffic": {
}, "rx": { "prefix": "" },
"caffeine": { "tx": { "prefix": "" }
"activated": {"prefix": " " }, },
"deactivated": { "prefix": " " } "network_traffic": {
}, "rx": { "prefix": "" },
"xrandr": { "tx": { "prefix": "" }
"on": { "prefix": " "}, },
"off": { "prefix": " " }, "mpd": {
"refresh": { "prefix": "" } "playing": { "prefix": "" },
}, "paused": { "prefix": "" },
"redshift": { "stopped": { "prefix": "" },
"day": { "prefix": "" }, "prev": { "prefix": "" },
"night": { "prefix": "" }, "next": { "prefix": "" },
"transition": { "prefix": "" } "shuffle-on": { "prefix": "" },
}, "shuffle-off": { "prefix": "" },
"docker_ps": { "repeat-on": { "prefix": "" },
"prefix": "" "repeat-off": { "prefix": "" }
}, },
"sensors": { "arch-update": {
"prefix": "" "prefix": " "
}, },
"sensors2": { "github": {
"temp": { "prefix": "" }, "prefix": "  "
"fan": { "prefix": "" }, },
"cpu": { "prefix": "" } "spotify": {
}, "prefix": "  "
"traffic":{ },
"rx": { "prefix": "" }, "publicip": {
"tx": { "prefix": "" } "prefix": "  "
}, },
"mpd": { "weather": {
"playing": { "prefix": "" }, "clouds": { "prefix": "" },
"paused": { "prefix": "" }, "rain": { "prefix": "" },
"stopped": { "prefix": "" }, "snow": { "prefix": "" },
"prev": { "prefix": "" }, "clear": { "prefix": "" },
"next": { "prefix": "" }, "thunder": { "prefix": "" }
"shuffle-on": { "prefix": "" }, },
"shuffle-off": { "prefix": "" }, "taskwarrior": {
"repeat-on": { "prefix": "" }, "prefix": "  "
"repeat-off": { "prefix": "" } },
}, "progress": {
"arch-update": { "copying": {
"prefix": " " "prefix": ""
},
"github": {
"prefix": "  "
},
"spotify": {
"prefix": "  "
},
"publicip": {
"prefix": "  "
},
"weather": {
"clouds": { "prefix": "" },
"rain": { "prefix": "" },
"snow": { "prefix": "" },
"clear": { "prefix": "" },
"thunder": { "prefix": "" }
},
"taskwarrior": {
"prefix": "  "
},
"progress": {
"copying": {
"prefix": ""
}
},
"git": {
"main": { "prefix": "" },
"new": { "prefix": "" },
"modified": { "prefix": "" },
"deleted": { "prefix": "" }
},
"dunst": {
"muted": { "prefix": ""},
"unmuted": { "prefix": "" }
},
"pihole": {
"enabled": { "prefix": "" },
"disabled": { "prefix": "" }
},
"vpn": {
"prefix": ""
} }
},
"git": {
"main": { "prefix": "" },
"new": { "prefix": "" },
"modified": { "prefix": "" },
"deleted": { "prefix": "" }
},
"dunst": {
"muted": { "prefix": "" },
"unmuted": { "prefix": "" }
},
"twmn": {
"muted": { "prefix": "" },
"unmuted": { "prefix": "" }
},
"pihole": {
"enabled": { "prefix": "" },
"disabled": { "prefix": "" }
},
"vpn": {
"prefix": ""
},
"system": {
"prefix": "  "
},
"sun": {
"prefix": ""
},
"rss": {
"prefix": ""
}
} }

View file

@ -1,153 +1,163 @@
{ {
"defaults": { "defaults": {
"separator": "\ue0b2", "padding": "\u2800", "separator": "\ue0b2",
"unknown": { "prefix": "\uf100" } "padding": "\u2800",
}, "unknown": { "prefix": "\uf100" }
"date": { "prefix": "\uf2d1" }, },
"time": { "prefix": "\uf3b3" }, "date": { "prefix": "\uf2d1" },
"datetime": { "prefix": "\uf3b3" }, "time": { "prefix": "\uf3b3" },
"memory": { "prefix": "\uf389" }, "datetime": { "prefix": "\uf3b3" },
"cpu": { "prefix": "\uf4b0" }, "memory": { "prefix": "\uf389" },
"disk": { "prefix": "\u26c1" }, "cpu": { "prefix": "\uf4b0" },
"dnf": { "prefix": "\uf2be" }, "disk": { "prefix": "\u26c1" },
"apt": { "prefix": "\uf2be" }, "dnf": { "prefix": "\uf2be" },
"pacman": { "prefix": "\uf2be" }, "apt": { "prefix": "\uf2be" },
"brightness": { "prefix": "\u263c" }, "pacman": { "prefix": "\uf2be" },
"load": { "prefix": "\uf13d" }, "brightness": { "prefix": "\u263c" },
"layout": { "prefix": "\uf38c" }, "load": { "prefix": "\uf13d" },
"layout-xkb": { "prefix": "\uf38c" }, "layout": { "prefix": "\uf38c" },
"todo": { "empty": {"prefix": "\uf453" }, "layout-xkb": { "prefix": "\uf38c" },
"items": {"prefix": "\uf454" }, "todo": {
"uptime": {"prefix": "\uf4c1" } "empty": { "prefix": "\uf453" },
}, "items": { "prefix": "\uf454" },
"zpool": { "uptime": { "prefix": "\uf4c1" }
"poolread": {"prefix": "\u26c1\uf3d6"}, },
"poolwrite": {"prefix": "\u26c1\uf3d5"}, "zpool": {
"ONLINE": {"prefix": "\u26c1"}, "poolread": { "prefix": "\u26c1\uf3d6" },
"FAULTED": {"prefix": "\u26c1\uf3bc"}, "poolwrite": { "prefix": "\u26c1\uf3d5" },
"DEGRADED": {"prefix": "\u26c1\uf3bc"} "ONLINE": { "prefix": "\u26c1" },
}, "FAULTED": { "prefix": "\u26c1\uf3bc" },
"cmus": { "DEGRADED": { "prefix": "\u26c1\uf3bc" }
"playing": { "prefix": "\uf488" }, },
"paused": { "prefix": "\uf210" }, "cmus": {
"stopped": { "prefix": "\uf24f" }, "playing": { "prefix": "\uf488" },
"prev": { "prefix": "\uf4ab" }, "paused": { "prefix": "\uf210" },
"next": { "prefix": "\uf4ad" }, "stopped": { "prefix": "\uf24f" },
"shuffle-on": { "prefix": "\uf4a8" }, "prev": { "prefix": "\uf4ab" },
"shuffle-off": { "prefix": "\uf453" }, "next": { "prefix": "\uf4ad" },
"repeat-on": { "prefix": "\uf459" }, "shuffle-on": { "prefix": "\uf4a8" },
"repeat-off": { "prefix": "\uf30f" } "shuffle-off": { "prefix": "\uf453" },
}, "repeat-on": { "prefix": "\uf459" },
"gpmdp": { "repeat-off": { "prefix": "\uf30f" }
"playing": { "prefix": "\uf488" }, },
"paused": { "prefix": "\uf210" }, "gpmdp": {
"stopped": { "prefix": "\uf24f" }, "playing": { "prefix": "\uf488" },
"prev": { "prefix": "\uf4ab" }, "paused": { "prefix": "\uf210" },
"next": { "prefix": "\uf4ad" } "stopped": { "prefix": "\uf24f" },
}, "prev": { "prefix": "\uf4ab" },
"pasink": { "next": { "prefix": "\uf4ad" }
"muted": { "prefix": "\uf3b9" }, },
"unmuted": { "prefix": "\uf3ba" } "pasink": {
}, "muted": { "prefix": "\uf3b9" },
"amixer": { "unmuted": { "prefix": "\uf3ba" }
"muted": { "prefix": "\uf3b9" }, },
"unmuted": { "prefix": "\uf3ba" } "amixer": {
}, "muted": { "prefix": "\uf3b9" },
"pasource": { "unmuted": { "prefix": "\uf3ba" }
"muted": { "prefix": "\uf395" }, },
"unmuted": { "prefix": "\uf2ec" } "pasource": {
}, "muted": { "prefix": "\uf395" },
"unmuted": { "prefix": "\uf2ec" }
},
"kernel": { "kernel": {
"prefix": "\uf17c" "prefix": "\uf17c"
}, },
"nic": { "nic": {
"wireless-up": { "prefix": "\uf25c" }, "wireless-up": { "prefix": "\uf25c" },
"wireless-down": { "prefix": "\uf3d0" }, "wireless-down": { "prefix": "\uf3d0" },
"wired-up": { "prefix": "\uf270" }, "wired-up": { "prefix": "\uf270" },
"wired-down": { "prefix": "\uf271" }, "wired-down": { "prefix": "\uf271" },
"tunnel-up": { "prefix": "\uf133" }, "tunnel-up": { "prefix": "\uf133" },
"tunnel-down": { "prefix": "\uf306" } "tunnel-down": { "prefix": "\uf306" }
}, },
"bluetooth": { "bluetooth": {
"ON": { "prefix": "\uf116" }, "ON": { "prefix": "\uf116" },
"OFF": { "prefix": "\uf116" }, "OFF": { "prefix": "\uf116" },
"?": { "prefix": "\uf116" } "?": { "prefix": "\uf116" }
},
"battery": {
"charged": { "prefix": "\uf113", "suffix": "\uf493" },
"AC": { "suffix": "\uf493" },
"charging": {
"prefix": ["\uf112", "\uf115", "\uf114", "", "\uf111"],
"suffix": "\uf493"
}, },
"battery": { "discharging-10": { "prefix": "\uf112", "suffix": "\uf3bc" },
"charged": { "prefix": "\uf113", "suffix": "\uf493" }, "discharging-25": { "prefix": "\uf115", "suffix": "\uf3e6" },
"AC": { "suffix": "\uf493" }, "discharging-50": { "prefix": "\uf115", "suffix": "\uf3e6" },
"charging": { "discharging-80": { "prefix": "\uf114", "suffix": "\uf3e6" },
"prefix": [ "\uf112", "\uf115", "\uf114", "", "\uf111" ], "discharging-100": { "prefix": "\uf113", "suffix": "\uf3e6" },
"suffix": "\uf493" "unknown-10": { "prefix": "\uf112", "suffix": "\uf3bc" },
}, "unknown-25": { "prefix": "\uf115", "suffix": "\uf142" },
"discharging-10": { "prefix": "\uf112", "suffix": "\uf3bc" }, "unknown-50": { "prefix": "\uf115", "suffix": "\uf142" },
"discharging-25": { "prefix": "\uf115", "suffix": "\uf3e6" }, "unknown-80": { "prefix": "\uf114", "suffix": "\uf142" },
"discharging-50": { "prefix": "\uf115", "suffix": "\uf3e6" }, "unknown-100": { "prefix": "\uf113", "suffix": "\uf142" },
"discharging-80": { "prefix": "\uf114", "suffix": "\uf3e6" }, "unlimited": { "prefix": "\uf402", "suffix": "\uf493" },
"discharging-100": { "prefix": "\uf113", "suffix": "\uf3e6" }, "estimate": { "prefix": "\uf402" }
"unknown-10": { "prefix": "\uf112", "suffix": "\uf3bc" }, },
"unknown-25": { "prefix": "\uf115", "suffix": "\uf142" }, "battery_all": {
"unknown-50": { "prefix": "\uf115", "suffix": "\uf142" }, "charged": { "prefix": "\uf113", "suffix": "\uf493" },
"unknown-80": { "prefix": "\uf114", "suffix": "\uf142" }, "AC": { "suffix": "\uf493" },
"unknown-100": { "prefix": "\uf113", "suffix": "\uf142" }, "charging": {
"unlimited": { "prefix": "\uf402", "suffix": "\uf493" }, "prefix": ["\uf112", "\uf115", "\uf114", "", "\uf111"],
"estimate": { "prefix": "\uf402" } "suffix": "\uf493"
}, },
"battery_all": { "discharging-10": { "prefix": "\uf112", "suffix": "\uf3bc" },
"charged": { "prefix": "\uf113", "suffix": "\uf493" }, "discharging-25": { "prefix": "\uf115", "suffix": "\uf3e6" },
"AC": { "suffix": "\uf493" }, "discharging-50": { "prefix": "\uf115", "suffix": "\uf3e6" },
"charging": { "discharging-80": { "prefix": "\uf114", "suffix": "\uf3e6" },
"prefix": [ "\uf112", "\uf115", "\uf114", "", "\uf111" ], "discharging-100": { "prefix": "\uf113", "suffix": "\uf3e6" },
"suffix": "\uf493" "unknown-10": { "prefix": "\uf112", "suffix": "\uf3bc" },
}, "unknown-25": { "prefix": "\uf115", "suffix": "\uf142" },
"discharging-10": { "prefix": "\uf112", "suffix": "\uf3bc" }, "unknown-50": { "prefix": "\uf115", "suffix": "\uf142" },
"discharging-25": { "prefix": "\uf115", "suffix": "\uf3e6" }, "unknown-80": { "prefix": "\uf114", "suffix": "\uf142" },
"discharging-50": { "prefix": "\uf115", "suffix": "\uf3e6" }, "unknown-100": { "prefix": "\uf113", "suffix": "\uf142" },
"discharging-80": { "prefix": "\uf114", "suffix": "\uf3e6" }, "unlimited": { "prefix": "\uf402", "suffix": "\uf493" },
"discharging-100": { "prefix": "\uf113", "suffix": "\uf3e6" }, "estimate": { "prefix": "\uf402" }
"unknown-10": { "prefix": "\uf112", "suffix": "\uf3bc" }, },
"unknown-25": { "prefix": "\uf115", "suffix": "\uf142" }, "caffeine": {
"unknown-50": { "prefix": "\uf115", "suffix": "\uf142" }, "activated": { "prefix": "\uf272\u3000\uf354" },
"unknown-80": { "prefix": "\uf114", "suffix": "\uf142" }, "deactivated": { "prefix": "\uf272\u3000\uf355" }
"unknown-100": { "prefix": "\uf113", "suffix": "\uf142" }, },
"unlimited": { "prefix": "\uf402", "suffix": "\uf493" }, "xrandr": {
"estimate": { "prefix": "\uf402" } "on": { "prefix": "\uf465\u3000\uf354" },
}, "off": { "prefix": "\uf465\u3000\uf355" }
"caffeine": { },
"activated": {"prefix": "\uf272\u3000\uf354" }, "deactivated": { "prefix": "\uf272\u3000\uf355" } "redshift": {
}, "day": { "prefix": "\uf4b6" },
"xrandr": { "night": { "prefix": "\uf467" },
"on": { "prefix": "\uf465\u3000\uf354"}, "off": { "prefix": "\uf465\u3000\uf355" } "transition": { "prefix": "\uf475" }
}, },
"redshift": {
"day": { "prefix": "\uf4b6" }, "night": { "prefix": "\uf467" }, "transition": { "prefix": "\uf475" }
},
"sensors": { "sensors": {
"prefix": "\uf3b6" "prefix": "\uf3b6"
}, },
"traffic":{ "traffic": {
"rx": { "prefix": "\uf365" }, "rx": { "prefix": "\uf365" },
"tx": { "prefix": "\uf35f" } "tx": { "prefix": "\uf35f" }
},
"network_traffic": {
"rx": { "prefix": "\uf365" },
"tx": { "prefix": "\uf35f" }
}, },
"mpd": { "mpd": {
"playing": { "prefix": "\uf488" }, "playing": { "prefix": "\uf488" },
"paused": { "prefix": "\uf210" }, "paused": { "prefix": "\uf210" },
"stopped": { "prefix": "\uf24f" }, "stopped": { "prefix": "\uf24f" },
"prev": { "prefix": "\uf4ab" }, "prev": { "prefix": "\uf4ab" },
"next": { "prefix": "\uf4ad" }, "next": { "prefix": "\uf4ad" },
"shuffle-on": { "prefix": "\uf4a8" }, "shuffle-on": { "prefix": "\uf4a8" },
"shuffle-off": { "prefix": "\uf453" }, "shuffle-off": { "prefix": "\uf453" },
"repeat-on": { "prefix": "\uf459" }, "repeat-on": { "prefix": "\uf459" },
"repeat-off": { "prefix": "\uf30f" } "repeat-off": { "prefix": "\uf30f" }
}, },
"github": { "github": {
"prefix": "\uf233" "prefix": "\uf233"
}, },
"spotify": { "spotify": {
"prefix": "\uf305" "prefix": "\uf305"
}, },
"publicip": { "publicip": {
"prefix": "\uf268" "prefix": "\uf268"
}, },
"weather": { "weather": {
"clouds": { "prefix": "\uf12b" }, "clouds": { "prefix": "\uf12b" },
@ -157,11 +167,23 @@
"thunder": { "prefix": "\uf4bd" } "thunder": { "prefix": "\uf4bd" }
}, },
"taskwarrior": { "taskwarrior": {
"prefix": "\uf454" "prefix": "\uf454"
}, },
"dunst": { "dunst": {
"muted": { "prefix": "\uf39a"}, "muted": { "prefix": "\uf39a" },
"unmuted": { "prefix": "\uf39b" } "unmuted": { "prefix": "\uf39b" }
} },
"twmn": {
"muted": { "prefix": "\uf1f6" },
"unmuted": { "prefix": "\uf0f3" }
},
"system": {
"prefix": " \uf2a9 "
},
"sun": {
"prefix": "\uf3b0"
},
"rss": {
"prefix": "\uf1ea"
}
} }