Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
928895befe
33 changed files with 1859 additions and 680 deletions
|
@ -2,7 +2,6 @@ sudo: false
|
|||
language: python
|
||||
python:
|
||||
- "2.7"
|
||||
- "3.3"
|
||||
- "3.4"
|
||||
- "3.5"
|
||||
- "3.6"
|
||||
|
|
12
PKGBUILD
12
PKGBUILD
|
@ -1,8 +1,9 @@
|
|||
# Maintainer: Tobias Witek <tobi@tobi-wan-kenobi.at>
|
||||
# Contributor: Daniel M. Capella <polycitizen@gmail.com>
|
||||
# Contributor: spookykidmm <https://github.com/spookykidmm>
|
||||
|
||||
pkgname=bumblebee-status
|
||||
pkgver=1.4.2
|
||||
pkgver=1.8.0
|
||||
pkgrel=1
|
||||
pkgdesc='Modular, theme-able status line generator for the i3 window manager'
|
||||
arch=('any')
|
||||
|
@ -22,9 +23,10 @@ optdepends=('xorg-xbacklight: to display a displays brightness'
|
|||
'i3ipc-python: display titlebar'
|
||||
'fakeroot: dependency of the pacman 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")
|
||||
sha512sums=('3a66fc469dd3b081337c9e213a1b2262f25f30977ee6ef65b9fa5a8b6aa341637832d1a5dbb74e30d68e2824e0d19d7a911eb3390dc6062707a552f429b483e8')
|
||||
sha512sums=('b985e6619856519a92bd1a9d2a762997e8920a0ef5007f20063fcb2f9afeb193de67e8b0737182576f79ee19b2dd3e6388bb9b987480b7bc19b537f64e9b5685')
|
||||
|
||||
package() {
|
||||
install -d "$pkgdir"/usr/bin \
|
||||
|
@ -32,8 +34,8 @@ package() {
|
|||
ln -s /usr/share/$pkgname/$pkgname "$pkgdir"/usr/bin/$pkgname
|
||||
|
||||
cd $pkgname-$pkgver
|
||||
cp -a --parents $pkgname bumblebee/{,modules/}*.py themes/{,icons/}*.json \
|
||||
"$pkgdir"/usr/share/$pkgname
|
||||
cp -a --parents $pkgname bumblebee/{,modules/}*.py themes/{,icons/}*.json $pkgdir/usr/share/$pkgname
|
||||
cp -r bin $pkgdir/usr/share/$pkgname/
|
||||
|
||||
install -Dm644 LICENSE "$pkgdir"/usr/share/licenses/$pkgname/LICENSE
|
||||
}
|
||||
|
|
16
README.md
16
README.md
|
@ -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)
|
||||
[![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)
|
||||
|
||||
|
@ -38,6 +38,10 @@ Explicitly unsupported Python versions: 3.2 (missing unicode literals)
|
|||
# Arch Linux
|
||||
$ 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
|
||||
# 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 {
|
||||
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')
|
||||
* 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')
|
||||
* dbus (for the module 'spotify')
|
||||
* 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')
|
||||
* pytz (for the module 'datetimetz')
|
||||
* localtz (for the module 'datetimetz')
|
||||
* suntime (for the module 'sun')
|
||||
* feedparser (for the module 'rss')
|
||||
|
||||
# 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)
|
||||
* zpool (for module 'zpool')
|
||||
* progress (for module 'progress')
|
||||
* i3exit (for module 'system')
|
||||
|
||||
# Examples
|
||||
Here are some screenshots for all themes that currently exist:
|
||||
|
|
|
@ -84,7 +84,7 @@ class Config(bumblebee.store.Store):
|
|||
|
||||
parameters = [item for sub in self._args.parameters for item in sub]
|
||||
for param in parameters:
|
||||
key, value = param.split("=")
|
||||
key, value = param.split("=", 1)
|
||||
self.set(key, value)
|
||||
|
||||
def modules(self):
|
||||
|
|
|
@ -41,6 +41,7 @@ class Module(object):
|
|||
self.error = None
|
||||
self._next = int(time.time())
|
||||
self._default_interval = 0
|
||||
self._engine = engine
|
||||
|
||||
self._configFile = None
|
||||
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:
|
||||
self._widgets = widgets if isinstance(widgets, list) else [widgets]
|
||||
|
||||
def theme(self):
|
||||
return self._engine.theme()
|
||||
|
||||
def widgets(self, widgets=None):
|
||||
"""Return the widgets to draw for this module"""
|
||||
if widgets:
|
||||
|
@ -160,6 +164,9 @@ class Engine(object):
|
|||
|
||||
self.input.start()
|
||||
|
||||
def theme(self):
|
||||
return self._theme
|
||||
|
||||
def _toggle_minimize(self, event):
|
||||
for module in self._modules:
|
||||
widget = module.widget_by_id(event["instance"])
|
||||
|
|
|
@ -25,15 +25,15 @@ def is_terminated():
|
|||
|
||||
def read_input(inp):
|
||||
"""Read i3bar input and execute callbacks"""
|
||||
epoll = select.epoll()
|
||||
epoll.register(sys.stdin.fileno(), select.EPOLLIN)
|
||||
poll = select.poll()
|
||||
poll.register(sys.stdin.fileno(), select.POLLIN)
|
||||
log.debug("starting click event processing")
|
||||
while inp.running:
|
||||
if is_terminated():
|
||||
return
|
||||
|
||||
try:
|
||||
events = epoll.poll(1)
|
||||
events = poll.poll(1000)
|
||||
except Exception:
|
||||
continue
|
||||
for fileno, event in events:
|
||||
|
@ -52,8 +52,7 @@ def read_input(inp):
|
|||
except ValueError as e:
|
||||
log.debug("failed to parse event: {}".format(e))
|
||||
log.debug("exiting click event processing")
|
||||
epoll.unregister(sys.stdin.fileno())
|
||||
epoll.close()
|
||||
poll.unregister(sys.stdin.fileno())
|
||||
inp.has_event = True
|
||||
inp.clean_exit = True
|
||||
|
||||
|
|
|
@ -29,11 +29,15 @@ class Module(bumblebee.engine.Module):
|
|||
return len(packages)
|
||||
return 0
|
||||
|
||||
@property
|
||||
def _format(self):
|
||||
return self.parameter("format", "Update Arch: {}")
|
||||
|
||||
def utilization(self, widget):
|
||||
return 'Update Arch: {}'.format(self.packages)
|
||||
return self._format.format(self.packages)
|
||||
|
||||
def hidden(self):
|
||||
return self.check_updates() == 0
|
||||
return self.check_updates() == 0
|
||||
|
||||
def update(self, widgets):
|
||||
self.packages = self.check_updates()
|
||||
|
|
|
@ -7,6 +7,7 @@ Parameters:
|
|||
* battery.warning : Warning threshold in % of remaining charge (defaults to 20)
|
||||
* 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.decorate : If set to "false", hides additional icons (charging, etc.) (defaults to True)
|
||||
"""
|
||||
|
||||
import os
|
||||
|
@ -47,6 +48,8 @@ class Module(bumblebee.engine.Module):
|
|||
self.capacity(widget)
|
||||
while len(widgets) > 0: del widgets[0]
|
||||
for widget in new_widgets:
|
||||
if bumblebee.util.asbool(self.parameter("decorate", True)) == False:
|
||||
widget.set("theme.exclude", "suffix")
|
||||
widgets.append(widget)
|
||||
self._widgets = widgets
|
||||
|
||||
|
|
|
@ -17,8 +17,11 @@ Parameters:
|
|||
from __future__ import absolute_import
|
||||
import datetime
|
||||
import locale
|
||||
import pytz
|
||||
import tzlocal
|
||||
try:
|
||||
import pytz
|
||||
import tzlocal
|
||||
except:
|
||||
pass
|
||||
import bumblebee.input
|
||||
import bumblebee.output
|
||||
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.RIGHT_MOUSE, cmd=self.prev_tz)
|
||||
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
|
||||
|
||||
l = locale.getdefaultlocale()
|
||||
|
@ -54,10 +60,13 @@ class Module(bumblebee.engine.Module):
|
|||
|
||||
def get_time(self, widget):
|
||||
try:
|
||||
tz = pytz.timezone(self._timezones[self._current_tz].strip())
|
||||
retval = datetime.datetime.now(tz=tzlocal.get_localzone()).astimezone(tz).strftime(self._fmt)
|
||||
except pytz.exceptions.UnknownTimeZoneError:
|
||||
retval = "[Unknown timezone: {}]".format(self._timezones[self._current_tz].strip())
|
||||
try:
|
||||
tz = pytz.timezone(self._timezones[self._current_tz].strip())
|
||||
retval = datetime.datetime.now(tz=tzlocal.get_localzone()).astimezone(tz).strftime(self._fmt)
|
||||
except pytz.exceptions.UnknownTimeZoneError:
|
||||
retval = "[Unknown timezone: {}]".format(self._timezones[self._current_tz].strip())
|
||||
except:
|
||||
retval = "[n/a]"
|
||||
|
||||
enc = locale.getpreferredencoding()
|
||||
if hasattr(retval, "decode"):
|
||||
|
|
68
bumblebee/modules/http_status.py
Normal file
68
bumblebee/modules/http_status.py
Normal 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
|
114
bumblebee/modules/network_traffic.py
Normal file
114
bumblebee/modules/network_traffic.py
Normal 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)
|
|
@ -1,68 +1,68 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays the pi-hole status (up/down) together with the number of ads that were blocked today
|
||||
Parameters:
|
||||
* 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)
|
||||
"""
|
||||
|
||||
import bumblebee.input
|
||||
import bumblebee.output
|
||||
import bumblebee.engine
|
||||
import requests
|
||||
|
||||
class Module(bumblebee.engine.Module):
|
||||
def __init__(self, engine, config):
|
||||
super(Module, self).__init__(engine, config,
|
||||
bumblebee.output.Widget(full_text=self.pihole_status)
|
||||
)
|
||||
|
||||
buttons = {"LEFT_CLICK":bumblebee.input.LEFT_MOUSE}
|
||||
self._pihole_address = self.parameter("address", "")
|
||||
|
||||
self._pihole_pw_hash = self.parameter("pwhash", "")
|
||||
self._pihole_status = None
|
||||
self._ads_blocked_today = "-"
|
||||
self.update_pihole_status()
|
||||
|
||||
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE,
|
||||
cmd=self.toggle_pihole_status)
|
||||
|
||||
def pihole_status(self, widget):
|
||||
if self._pihole_status is None:
|
||||
return "pi-hole unknown"
|
||||
return "pi-hole " + ("up/" + self._ads_blocked_today if self._pihole_status else "down")
|
||||
|
||||
def update_pihole_status(self):
|
||||
try:
|
||||
data = requests.get(self._pihole_address + "/admin/api.php?summary").json()
|
||||
self._pihole_status = True if data["status"] == "enabled" else False
|
||||
self._ads_blocked_today = data["ads_blocked_today"]
|
||||
except:
|
||||
self._pihole_status = None
|
||||
|
||||
def toggle_pihole_status(self, widget):
|
||||
if self._pihole_status is not None:
|
||||
try:
|
||||
req = None
|
||||
if self._pihole_status:
|
||||
req = requests.get(self._pihole_address + "/admin/api.php?disable&auth=" + self._pihole_pw_hash)
|
||||
else:
|
||||
req = requests.get(self._pihole_address + "/admin/api.php?enable&auth=" + self._pihole_pw_hash)
|
||||
if req is not None:
|
||||
if req.status_code == 200:
|
||||
status = req.json()["status"]
|
||||
self._pihole_status = False if status == "disabled" else True
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def update(self, widgets):
|
||||
self.update_pihole_status()
|
||||
|
||||
def state(self, widget):
|
||||
if self._pihole_status is None:
|
||||
return []
|
||||
elif self._pihole_status:
|
||||
return ["enabled"]
|
||||
return ["disabled", "warning"]
|
||||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays the pi-hole status (up/down) together with the number of ads that were blocked today
|
||||
Parameters:
|
||||
* 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)
|
||||
"""
|
||||
|
||||
import bumblebee.input
|
||||
import bumblebee.output
|
||||
import bumblebee.engine
|
||||
import requests
|
||||
|
||||
class Module(bumblebee.engine.Module):
|
||||
def __init__(self, engine, config):
|
||||
super(Module, self).__init__(engine, config,
|
||||
bumblebee.output.Widget(full_text=self.pihole_status)
|
||||
)
|
||||
|
||||
buttons = {"LEFT_CLICK":bumblebee.input.LEFT_MOUSE}
|
||||
self._pihole_address = self.parameter("address", "")
|
||||
|
||||
self._pihole_pw_hash = self.parameter("pwhash", "")
|
||||
self._pihole_status = None
|
||||
self._ads_blocked_today = "-"
|
||||
self.update_pihole_status()
|
||||
|
||||
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE,
|
||||
cmd=self.toggle_pihole_status)
|
||||
|
||||
def pihole_status(self, widget):
|
||||
if self._pihole_status is None:
|
||||
return "pi-hole unknown"
|
||||
return "pi-hole " + ("up/" + self._ads_blocked_today if self._pihole_status else "down")
|
||||
|
||||
def update_pihole_status(self):
|
||||
try:
|
||||
data = requests.get(self._pihole_address + "/admin/api.php?summary").json()
|
||||
self._pihole_status = True if data["status"] == "enabled" else False
|
||||
self._ads_blocked_today = data["ads_blocked_today"]
|
||||
except:
|
||||
self._pihole_status = None
|
||||
|
||||
def toggle_pihole_status(self, widget):
|
||||
if self._pihole_status is not None:
|
||||
try:
|
||||
req = None
|
||||
if self._pihole_status:
|
||||
req = requests.get(self._pihole_address + "/admin/api.php?disable&auth=" + self._pihole_pw_hash)
|
||||
else:
|
||||
req = requests.get(self._pihole_address + "/admin/api.php?enable&auth=" + self._pihole_pw_hash)
|
||||
if req is not None:
|
||||
if req.status_code == 200:
|
||||
status = req.json()["status"]
|
||||
self._pihole_status = False if status == "disabled" else True
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def update(self, widgets):
|
||||
self.update_pihole_status()
|
||||
|
||||
def state(self, widget):
|
||||
if self._pihole_status is None:
|
||||
return []
|
||||
elif self._pihole_status:
|
||||
return ["enabled"]
|
||||
return ["disabled", "warning"]
|
||||
|
|
|
@ -4,21 +4,33 @@
|
|||
|
||||
Requires the following executable:
|
||||
* 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
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
import bumblebee.input
|
||||
import bumblebee.output
|
||||
import bumblebee.engine
|
||||
|
||||
|
||||
def is_terminated():
|
||||
for thread in threading.enumerate():
|
||||
if thread.name == "MainThread" and not thread.is_alive():
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_redshift_value(widget):
|
||||
|
||||
def get_redshift_value(widget, location, lat, lon):
|
||||
while True:
|
||||
if is_terminated():
|
||||
return
|
||||
|
@ -31,8 +43,14 @@ def get_redshift_value(widget):
|
|||
break
|
||||
widget.get("condition").release()
|
||||
|
||||
command = ["redshift", "-p", "-l"]
|
||||
if location == "manual":
|
||||
command.append(lat + ":" + lon)
|
||||
else:
|
||||
command.append("geoclue2")
|
||||
|
||||
try:
|
||||
res = bumblebee.util.execute("redshift -p")
|
||||
res = bumblebee.util.execute(" ".join(command))
|
||||
except Exception:
|
||||
res = ""
|
||||
widget.set("temp", "n/a")
|
||||
|
@ -52,14 +70,40 @@ def get_redshift_value(widget):
|
|||
widget.set("state", "transition")
|
||||
widget.set("transition", " ".join(line.split(" ")[2:]))
|
||||
|
||||
|
||||
class Module(bumblebee.engine.Module):
|
||||
def __init__(self, engine, config):
|
||||
widget = bumblebee.output.Widget(full_text=self.text)
|
||||
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._condition = threading.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._condition.acquire()
|
||||
self._condition.notify()
|
||||
|
|
311
bumblebee/modules/rss.py
Normal file
311
bumblebee/modules/rss.py
Normal 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'>★</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
111
bumblebee/modules/sun.py
Normal 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
|
83
bumblebee/modules/system.py
Normal file
83
bumblebee/modules/system.py
Normal 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 []
|
|
@ -103,7 +103,8 @@ class Module(bumblebee.engine.Module):
|
|||
widget = self.create_widget(widgets, name, attributes={"theme.minwidth": "1000.00MB"})
|
||||
prev = self._prev.get(name, 0)
|
||||
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]
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
||||
|
|
39
bumblebee/modules/twmn.py
Normal file
39
bumblebee/modules/twmn.py
Normal 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"]
|
81
bumblebee/modules/vault.py
Normal file
81
bumblebee/modules/vault.py
Normal 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
|
|
@ -1,95 +1,95 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
""" 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
|
||||
a VPN connection using that profile.
|
||||
|
||||
Prerequisites:
|
||||
* nmcli needs to be installed and configured properly.
|
||||
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!
|
||||
|
||||
e.g: to import a openvpn profile via nmcli:
|
||||
sudo nmcli connection import type openvpn file </path/to/your/openvpn/profile.ovpn>
|
||||
"""
|
||||
|
||||
import logging
|
||||
import bumblebee.input
|
||||
import bumblebee.output
|
||||
import bumblebee.engine
|
||||
import functools
|
||||
|
||||
class Module(bumblebee.engine.Module):
|
||||
def __init__(self, engine, config):
|
||||
super(Module, self).__init__(engine, config,
|
||||
bumblebee.output.Widget(full_text=self.vpn_status)
|
||||
)
|
||||
|
||||
self._connected_vpn_profile = None
|
||||
self._selected_vpn_profile = None
|
||||
|
||||
res = bumblebee.util.execute("nmcli -g NAME,TYPE c")
|
||||
lines = res.splitlines()
|
||||
|
||||
self._vpn_profiles = []
|
||||
for line in lines:
|
||||
info = line.split(':')
|
||||
try:
|
||||
if info[1] == "vpn":
|
||||
self._vpn_profiles.append(info[0])
|
||||
except:
|
||||
pass
|
||||
|
||||
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE,
|
||||
cmd=self.popup)
|
||||
|
||||
def update(self, widgets):
|
||||
try:
|
||||
res = bumblebee.util.execute("nmcli -g NAME,TYPE,DEVICE con")
|
||||
lines = res.splitlines()
|
||||
self._connected_vpn_profile = None
|
||||
for line in lines:
|
||||
info = line.split(':')
|
||||
if info[1] == "vpn" and info[2] != "":
|
||||
self._connected_vpn_profile = info[0]
|
||||
|
||||
except Exception as e:
|
||||
logging.exception("Couldn't get VPN status")
|
||||
self._connected_vpn_profile = None
|
||||
|
||||
def vpn_status(self, widget):
|
||||
if self._connected_vpn_profile is None:
|
||||
return "off"
|
||||
return self._connected_vpn_profile
|
||||
|
||||
def _on_vpn_disconnect(self):
|
||||
try:
|
||||
bumblebee.util.execute("nmcli c down " + self._connected_vpn_profile)
|
||||
self._connected_vpn_profile = None
|
||||
except Exception as e:
|
||||
logging.exception("Couldn't disconnect VPN connection")
|
||||
|
||||
def _on_vpn_connect(self, name):
|
||||
self._selected_vpn_profile = name
|
||||
|
||||
try:
|
||||
bumblebee.util.execute("nmcli c up " + self._selected_vpn_profile)
|
||||
self._connected_vpn_profile = name
|
||||
except Exception as e:
|
||||
logging.exception("Couldn't establish VPN connection")
|
||||
self._connected_vpn_profile = None
|
||||
|
||||
def popup(self, widget):
|
||||
menu = bumblebee.popup_v2.PopupMenu()
|
||||
|
||||
if self._connected_vpn_profile is not None:
|
||||
menu.add_menuitem("Disconnect", callback=self._on_vpn_disconnect)
|
||||
for vpn_profile in self._vpn_profiles:
|
||||
if self._connected_vpn_profile is not None and self._connected_vpn_profile == vpn_profile:
|
||||
continue
|
||||
menu.add_menuitem(vpn_profile, callback=functools.partial(self._on_vpn_connect, vpn_profile))
|
||||
menu.show(widget)
|
||||
|
||||
def state(self, widget):
|
||||
return []
|
||||
# pylint: disable=C0111,R0903
|
||||
|
||||
""" 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
|
||||
a VPN connection using that profile.
|
||||
|
||||
Prerequisites:
|
||||
* nmcli needs to be installed and configured properly.
|
||||
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!
|
||||
|
||||
e.g: to import a openvpn profile via nmcli:
|
||||
sudo nmcli connection import type openvpn file </path/to/your/openvpn/profile.ovpn>
|
||||
"""
|
||||
|
||||
import logging
|
||||
import bumblebee.input
|
||||
import bumblebee.output
|
||||
import bumblebee.engine
|
||||
import functools
|
||||
|
||||
class Module(bumblebee.engine.Module):
|
||||
def __init__(self, engine, config):
|
||||
super(Module, self).__init__(engine, config,
|
||||
bumblebee.output.Widget(full_text=self.vpn_status)
|
||||
)
|
||||
|
||||
self._connected_vpn_profile = None
|
||||
self._selected_vpn_profile = None
|
||||
|
||||
res = bumblebee.util.execute("nmcli -g NAME,TYPE c")
|
||||
lines = res.splitlines()
|
||||
|
||||
self._vpn_profiles = []
|
||||
for line in lines:
|
||||
info = line.split(':')
|
||||
try:
|
||||
if info[1] == "vpn":
|
||||
self._vpn_profiles.append(info[0])
|
||||
except:
|
||||
pass
|
||||
|
||||
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE,
|
||||
cmd=self.popup)
|
||||
|
||||
def update(self, widgets):
|
||||
try:
|
||||
res = bumblebee.util.execute("nmcli -g NAME,TYPE,DEVICE con")
|
||||
lines = res.splitlines()
|
||||
self._connected_vpn_profile = None
|
||||
for line in lines:
|
||||
info = line.split(':')
|
||||
if info[1] == "vpn" and info[2] != "":
|
||||
self._connected_vpn_profile = info[0]
|
||||
|
||||
except Exception as e:
|
||||
logging.exception("Couldn't get VPN status")
|
||||
self._connected_vpn_profile = None
|
||||
|
||||
def vpn_status(self, widget):
|
||||
if self._connected_vpn_profile is None:
|
||||
return "off"
|
||||
return self._connected_vpn_profile
|
||||
|
||||
def _on_vpn_disconnect(self):
|
||||
try:
|
||||
bumblebee.util.execute("nmcli c down " + self._connected_vpn_profile)
|
||||
self._connected_vpn_profile = None
|
||||
except Exception as e:
|
||||
logging.exception("Couldn't disconnect VPN connection")
|
||||
|
||||
def _on_vpn_connect(self, name):
|
||||
self._selected_vpn_profile = name
|
||||
|
||||
try:
|
||||
bumblebee.util.execute("nmcli c up " + self._selected_vpn_profile)
|
||||
self._connected_vpn_profile = name
|
||||
except Exception as e:
|
||||
logging.exception("Couldn't establish VPN connection")
|
||||
self._connected_vpn_profile = None
|
||||
|
||||
def popup(self, widget):
|
||||
menu = bumblebee.popup_v2.PopupMenu()
|
||||
|
||||
if self._connected_vpn_profile is not None:
|
||||
menu.add_menuitem("Disconnect", callback=self._on_vpn_disconnect)
|
||||
for vpn_profile in self._vpn_profiles:
|
||||
if self._connected_vpn_profile is not None and self._connected_vpn_profile == vpn_profile:
|
||||
continue
|
||||
menu.add_menuitem(vpn_profile, callback=functools.partial(self._on_vpn_connect, vpn_profile))
|
||||
menu.show(widget)
|
||||
|
||||
def state(self, widget):
|
||||
return []
|
||||
|
|
|
@ -118,8 +118,6 @@ class Module(bumblebee.engine.Module):
|
|||
self._temperature = int(weather['main']['temp'])
|
||||
self._weather = weather['weather'][0]['main'].lower()
|
||||
self._valid = True
|
||||
except RequestException:
|
||||
self._valid = False
|
||||
except Exception:
|
||||
self._valid = False
|
||||
|
||||
|
|
|
@ -13,13 +13,27 @@ except ImportError:
|
|||
|
||||
import functools
|
||||
|
||||
|
||||
class PopupMenu(object):
|
||||
def __init__(self):
|
||||
self._root = tk.Tk()
|
||||
self._root.withdraw()
|
||||
self._menu = tk.Menu(self._root)
|
||||
self._menu.bind("<FocusOut>", self._on_focus_out)
|
||||
def __init__(self, parent=None, leave=True):
|
||||
|
||||
if not parent:
|
||||
self._root = tk.Tk()
|
||||
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):
|
||||
self._root.destroy()
|
||||
|
@ -28,13 +42,15 @@ class PopupMenu(object):
|
|||
self._root.destroy()
|
||||
callback()
|
||||
|
||||
def add_cascade(self, menuitem, submenu):
|
||||
self._menu.add_cascade(label=menuitem, menu=submenu.menu())
|
||||
|
||||
def add_menuitem(self, menuitem, 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:
|
||||
self._menu.tk_popup(event['x'], event['y'])
|
||||
self._menu.tk_popup(event['x'] + offset_x, event['y'] + offset_y)
|
||||
finally:
|
||||
self._menu.grab_release()
|
||||
self._root.mainloop()
|
||||
|
|
|
@ -105,6 +105,9 @@ class Theme(object):
|
|||
if icon is 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):
|
||||
"""Return padding for widget"""
|
||||
return self._get(widget, "padding", "")
|
||||
|
@ -223,6 +226,9 @@ class Theme(object):
|
|||
if not self._widget:
|
||||
self._widget = widget
|
||||
|
||||
if self._widget.get("theme.exclude", "") == name:
|
||||
return None
|
||||
|
||||
if self._widget != widget:
|
||||
self._prevbg = self.bg(self._widget)
|
||||
self._widget = widget
|
||||
|
@ -238,7 +244,8 @@ class Theme(object):
|
|||
states = widget.state()
|
||||
if name not 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 = widget.get("theme.{}".format(name), value)
|
||||
|
|
BIN
screenshots/http_status.png
Normal file
BIN
screenshots/http_status.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
BIN
screenshots/network_traffic.gif
Normal file
BIN
screenshots/network_traffic.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
BIN
screenshots/vault.png
Normal file
BIN
screenshots/vault.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
|
@ -14,7 +14,7 @@ def rand(cnt):
|
|||
return "".join(random.choice("abcdefghijklmnopqrstuvwxyz0123456789") for i in range(cnt))
|
||||
|
||||
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()
|
||||
|
||||
|
@ -33,19 +33,19 @@ def teardown_test(test):
|
|||
test._select.stop()
|
||||
test.popen.cleanup()
|
||||
|
||||
def epoll_mock(module=""):
|
||||
def poll_mock(module=""):
|
||||
if len(module) > 0: module = "{}.".format(module)
|
||||
|
||||
stdin = mock.patch("{}sys.stdin".format(module))
|
||||
select = mock.patch("{}select".format(module))
|
||||
epoll = mock.Mock()
|
||||
poll = mock.Mock()
|
||||
|
||||
stdin_mock = stdin.start()
|
||||
select_mock = select.start()
|
||||
|
||||
stdin_mock.fileno.return_value = 1
|
||||
select_mock.epoll.return_value = epoll
|
||||
epoll.poll.return_value = [(stdin_mock.fileno.return_value, 100)]
|
||||
select_mock.poll.return_value = poll
|
||||
poll.poll.return_value = [(stdin_mock.fileno.return_value, 100)]
|
||||
|
||||
return stdin, select, stdin_mock, select_mock
|
||||
|
||||
|
|
49
tests/modules/test_http_status.py
Normal file
49
tests/modules/test_http_status.py
Normal 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
|
|
@ -20,10 +20,10 @@ class TestI3BarInput(unittest.TestCase):
|
|||
self.popen = mocks.MockPopen()
|
||||
|
||||
self.stdin.fileno.return_value = 1
|
||||
epoll = mock.Mock()
|
||||
self.select.epoll.return_value = epoll
|
||||
poll = mock.Mock()
|
||||
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.id = mocks.rand(10)
|
||||
|
|
|
@ -118,6 +118,14 @@ class TestTheme(unittest.TestCase):
|
|||
# widget theme instead (i.e. no fallback to a more general state theme)
|
||||
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):
|
||||
self.assertEquals(self.validThemeSeparator, self.theme.separator(self.anyWidget))
|
||||
|
||||
|
|
|
@ -1,133 +1,307 @@
|
|||
{
|
||||
"defaults": {
|
||||
"padding": " "
|
||||
},
|
||||
"memory": { "prefix": "ram" },
|
||||
"cpu": { "prefix": "cpu" },
|
||||
"disk": { "prefix": "hdd" },
|
||||
"dnf": { "prefix": "dnf" },
|
||||
"apt": { "prefix": "apt" },
|
||||
"brightness": { "prefix": "o" },
|
||||
"cmus": {
|
||||
"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]" }
|
||||
},
|
||||
"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" }
|
||||
"defaults": {
|
||||
"padding": " "
|
||||
},
|
||||
"memory": {
|
||||
"prefix": "ram"
|
||||
},
|
||||
"cpu": {
|
||||
"prefix": "cpu"
|
||||
},
|
||||
"disk": {
|
||||
"prefix": "hdd"
|
||||
},
|
||||
"dnf": {
|
||||
"prefix": "dnf"
|
||||
},
|
||||
"apt": {
|
||||
"prefix": "apt"
|
||||
},
|
||||
"brightness": {
|
||||
"prefix": "o"
|
||||
},
|
||||
"cmus": {
|
||||
"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]"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,206 +1,226 @@
|
|||
{
|
||||
"defaults": {
|
||||
"separator": "", "padding": " ",
|
||||
"unknown": { "prefix": "" }
|
||||
},
|
||||
"date": { "prefix": "" },
|
||||
"time": { "prefix": "" },
|
||||
"datetime": { "prefix": "" },
|
||||
"datetz": { "prefix": "" },
|
||||
"timetz": { "prefix": "" },
|
||||
"datetimetz": { "prefix": "" },
|
||||
"memory": { "prefix": "" },
|
||||
"cpu": { "prefix": "" },
|
||||
"disk": { "prefix": "" },
|
||||
"dnf": { "prefix": "" },
|
||||
"apt": { "prefix": "" },
|
||||
"pacman": { "prefix": "" },
|
||||
"brightness": { "prefix": "" },
|
||||
"load": { "prefix": "" },
|
||||
"layout": { "prefix": "" },
|
||||
"layout-xkb": { "prefix": "" },
|
||||
"notmuch_count": { "empty": {"prefix": "\uf0e0" },
|
||||
"items": {"prefix": "\uf0e0" }
|
||||
},
|
||||
"todo": { "empty": {"prefix": "" },
|
||||
"items": {"prefix": "" },
|
||||
"uptime": {"prefix": "" }
|
||||
},
|
||||
"zpool": {
|
||||
"poolread": {"prefix": "→ "},
|
||||
"poolwrite": {"prefix": "← "},
|
||||
"ONLINE": {"prefix": ""},
|
||||
"FAULTED": {"prefix": "!"},
|
||||
"DEGRADED": {"prefix": "!"}
|
||||
},
|
||||
"cmus": {
|
||||
"playing": { "prefix": "" },
|
||||
"paused": { "prefix": "" },
|
||||
"stopped": { "prefix": "" },
|
||||
"prev": { "prefix": "" },
|
||||
"next": { "prefix": "" },
|
||||
"shuffle-on": { "prefix": "" },
|
||||
"shuffle-off": { "prefix": "" },
|
||||
"repeat-on": { "prefix": "" },
|
||||
"repeat-off": { "prefix": "" }
|
||||
},
|
||||
"gpmdp": {
|
||||
"playing": { "prefix": "" },
|
||||
"paused": { "prefix": "" },
|
||||
"stopped": { "prefix": "" },
|
||||
"prev": { "prefix": "" },
|
||||
"next": { "prefix": "" }
|
||||
},
|
||||
"pasink": {
|
||||
"muted": { "prefix": "" },
|
||||
"unmuted": { "prefix": "" }
|
||||
},
|
||||
"amixer": {
|
||||
"muted": { "prefix": "" },
|
||||
"unmuted": { "prefix": "" }
|
||||
},
|
||||
"pasource": {
|
||||
"muted": { "prefix": "" },
|
||||
"unmuted": { "prefix": "" }
|
||||
},
|
||||
"kernel": {
|
||||
"prefix": "\uf17c"
|
||||
"defaults": {
|
||||
"separator": "",
|
||||
"padding": " ",
|
||||
"unknown": { "prefix": "" }
|
||||
},
|
||||
"date": { "prefix": "" },
|
||||
"time": { "prefix": "" },
|
||||
"datetime": { "prefix": "" },
|
||||
"datetz": { "prefix": "" },
|
||||
"timetz": { "prefix": "" },
|
||||
"datetimetz": { "prefix": "" },
|
||||
"memory": { "prefix": "" },
|
||||
"cpu": { "prefix": "" },
|
||||
"disk": { "prefix": "" },
|
||||
"dnf": { "prefix": "" },
|
||||
"apt": { "prefix": "" },
|
||||
"pacman": { "prefix": "" },
|
||||
"brightness": { "prefix": "" },
|
||||
"load": { "prefix": "" },
|
||||
"layout": { "prefix": "" },
|
||||
"layout-xkb": { "prefix": "" },
|
||||
"notmuch_count": {
|
||||
"empty": { "prefix": "\uf0e0" },
|
||||
"items": { "prefix": "\uf0e0" }
|
||||
},
|
||||
"todo": {
|
||||
"empty": { "prefix": "" },
|
||||
"items": { "prefix": "" },
|
||||
"uptime": { "prefix": "" }
|
||||
},
|
||||
"zpool": {
|
||||
"poolread": { "prefix": "→ " },
|
||||
"poolwrite": { "prefix": "← " },
|
||||
"ONLINE": { "prefix": "" },
|
||||
"FAULTED": { "prefix": "!" },
|
||||
"DEGRADED": { "prefix": "!" }
|
||||
},
|
||||
"cmus": {
|
||||
"playing": { "prefix": "" },
|
||||
"paused": { "prefix": "" },
|
||||
"stopped": { "prefix": "" },
|
||||
"prev": { "prefix": "" },
|
||||
"next": { "prefix": "" },
|
||||
"shuffle-on": { "prefix": "" },
|
||||
"shuffle-off": { "prefix": "" },
|
||||
"repeat-on": { "prefix": "" },
|
||||
"repeat-off": { "prefix": "" }
|
||||
},
|
||||
"gpmdp": {
|
||||
"playing": { "prefix": "" },
|
||||
"paused": { "prefix": "" },
|
||||
"stopped": { "prefix": "" },
|
||||
"prev": { "prefix": "" },
|
||||
"next": { "prefix": "" }
|
||||
},
|
||||
"pasink": {
|
||||
"muted": { "prefix": "" },
|
||||
"unmuted": { "prefix": "" }
|
||||
},
|
||||
"amixer": {
|
||||
"muted": { "prefix": "" },
|
||||
"unmuted": { "prefix": "" }
|
||||
},
|
||||
"pasource": {
|
||||
"muted": { "prefix": "" },
|
||||
"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": {
|
||||
"wireless-up": { "prefix": "" },
|
||||
"wireless-down": { "prefix": "" },
|
||||
"wired-up": { "prefix": "" },
|
||||
"wired-down": { "prefix": "" },
|
||||
"tunnel-up": { "prefix": "" },
|
||||
"tunnel-down": { "prefix": "" }
|
||||
},
|
||||
"bluetooth": {
|
||||
"ON": { "prefix": "" },
|
||||
"OFF": { "prefix": "" },
|
||||
"?": { "prefix": "" }
|
||||
"discharging-10": { "prefix": "", "suffix": "" },
|
||||
"discharging-25": { "prefix": "", "suffix": "" },
|
||||
"discharging-50": { "prefix": "", "suffix": "" },
|
||||
"discharging-80": { "prefix": "", "suffix": "" },
|
||||
"discharging-100": { "prefix": "", "suffix": "" },
|
||||
"unlimited": { "prefix": "", "suffix": "" },
|
||||
"estimate": { "prefix": "" },
|
||||
"unknown-10": { "prefix": "", "suffix": "" },
|
||||
"unknown-25": { "prefix": "", "suffix": "" },
|
||||
"unknown-50": { "prefix": "", "suffix": "" },
|
||||
"unknown-80": { "prefix": "", "suffix": "" },
|
||||
"unknown-100": { "prefix": "", "suffix": "" }
|
||||
},
|
||||
"battery_all": {
|
||||
"charged": { "prefix": "", "suffix": "" },
|
||||
"AC": { "suffix": "" },
|
||||
"charging": {
|
||||
"prefix": ["", "", "", "", ""],
|
||||
"suffix": ""
|
||||
},
|
||||
"battery": {
|
||||
"charged": { "prefix": "", "suffix": "" },
|
||||
"AC": { "suffix": "" },
|
||||
"charging": {
|
||||
"prefix": [ "", "", "", "", "" ],
|
||||
"suffix": ""
|
||||
},
|
||||
"discharging-10": { "prefix": "", "suffix": "" },
|
||||
"discharging-25": { "prefix": "", "suffix": "" },
|
||||
"discharging-50": { "prefix": "", "suffix": "" },
|
||||
"discharging-80": { "prefix": "", "suffix": "" },
|
||||
"discharging-100": { "prefix": "", "suffix": "" },
|
||||
"unlimited": { "prefix": "", "suffix": "" },
|
||||
"estimate": { "prefix": "" },
|
||||
"unknown-10": { "prefix": "", "suffix": "" },
|
||||
"unknown-25": { "prefix": "", "suffix": "" },
|
||||
"unknown-50": { "prefix": "", "suffix": "" },
|
||||
"unknown-80": { "prefix": "", "suffix": "" },
|
||||
"unknown-100": { "prefix": "", "suffix": "" }
|
||||
},
|
||||
"battery_all": {
|
||||
"charged": { "prefix": "", "suffix": "" },
|
||||
"AC": { "suffix": "" },
|
||||
"charging": {
|
||||
"prefix": [ "", "", "", "", "" ],
|
||||
"suffix": ""
|
||||
},
|
||||
"discharging-10": { "prefix": "", "suffix": "" },
|
||||
"discharging-25": { "prefix": "", "suffix": "" },
|
||||
"discharging-50": { "prefix": "", "suffix": "" },
|
||||
"discharging-80": { "prefix": "", "suffix": "" },
|
||||
"discharging-100": { "prefix": "", "suffix": "" },
|
||||
"unlimited": { "prefix": "", "suffix": "" },
|
||||
"estimate": { "prefix": "" },
|
||||
"unknown-10": { "prefix": "", "suffix": "" },
|
||||
"unknown-25": { "prefix": "", "suffix": "" },
|
||||
"unknown-50": { "prefix": "", "suffix": "" },
|
||||
"unknown-80": { "prefix": "", "suffix": "" },
|
||||
"unknown-100": { "prefix": "", "suffix": "" }
|
||||
},
|
||||
"caffeine": {
|
||||
"activated": {"prefix": " " },
|
||||
"deactivated": { "prefix": " " }
|
||||
},
|
||||
"xrandr": {
|
||||
"on": { "prefix": " "},
|
||||
"off": { "prefix": " " },
|
||||
"refresh": { "prefix": "" }
|
||||
},
|
||||
"redshift": {
|
||||
"day": { "prefix": "" },
|
||||
"night": { "prefix": "" },
|
||||
"transition": { "prefix": "" }
|
||||
},
|
||||
"docker_ps": {
|
||||
"prefix": ""
|
||||
},
|
||||
"sensors": {
|
||||
"prefix": ""
|
||||
},
|
||||
"sensors2": {
|
||||
"temp": { "prefix": "" },
|
||||
"fan": { "prefix": "" },
|
||||
"cpu": { "prefix": "" }
|
||||
},
|
||||
"traffic":{
|
||||
"rx": { "prefix": "" },
|
||||
"tx": { "prefix": "" }
|
||||
},
|
||||
"mpd": {
|
||||
"playing": { "prefix": "" },
|
||||
"paused": { "prefix": "" },
|
||||
"stopped": { "prefix": "" },
|
||||
"prev": { "prefix": "" },
|
||||
"next": { "prefix": "" },
|
||||
"shuffle-on": { "prefix": "" },
|
||||
"shuffle-off": { "prefix": "" },
|
||||
"repeat-on": { "prefix": "" },
|
||||
"repeat-off": { "prefix": "" }
|
||||
},
|
||||
"arch-update": {
|
||||
"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": ""
|
||||
"discharging-10": { "prefix": "", "suffix": "" },
|
||||
"discharging-25": { "prefix": "", "suffix": "" },
|
||||
"discharging-50": { "prefix": "", "suffix": "" },
|
||||
"discharging-80": { "prefix": "", "suffix": "" },
|
||||
"discharging-100": { "prefix": "", "suffix": "" },
|
||||
"unlimited": { "prefix": "", "suffix": "" },
|
||||
"estimate": { "prefix": "" },
|
||||
"unknown-10": { "prefix": "", "suffix": "" },
|
||||
"unknown-25": { "prefix": "", "suffix": "" },
|
||||
"unknown-50": { "prefix": "", "suffix": "" },
|
||||
"unknown-80": { "prefix": "", "suffix": "" },
|
||||
"unknown-100": { "prefix": "", "suffix": "" }
|
||||
},
|
||||
"caffeine": {
|
||||
"activated": { "prefix": " " },
|
||||
"deactivated": { "prefix": " " }
|
||||
},
|
||||
"xrandr": {
|
||||
"on": { "prefix": " " },
|
||||
"off": { "prefix": " " },
|
||||
"refresh": { "prefix": "" }
|
||||
},
|
||||
"redshift": {
|
||||
"day": { "prefix": "" },
|
||||
"night": { "prefix": "" },
|
||||
"transition": { "prefix": "" }
|
||||
},
|
||||
"docker_ps": {
|
||||
"prefix": ""
|
||||
},
|
||||
"sensors": {
|
||||
"prefix": ""
|
||||
},
|
||||
"sensors2": {
|
||||
"temp": { "prefix": "" },
|
||||
"fan": { "prefix": "" },
|
||||
"cpu": { "prefix": "" }
|
||||
},
|
||||
"traffic": {
|
||||
"rx": { "prefix": "" },
|
||||
"tx": { "prefix": "" }
|
||||
},
|
||||
"network_traffic": {
|
||||
"rx": { "prefix": "" },
|
||||
"tx": { "prefix": "" }
|
||||
},
|
||||
"mpd": {
|
||||
"playing": { "prefix": "" },
|
||||
"paused": { "prefix": "" },
|
||||
"stopped": { "prefix": "" },
|
||||
"prev": { "prefix": "" },
|
||||
"next": { "prefix": "" },
|
||||
"shuffle-on": { "prefix": "" },
|
||||
"shuffle-off": { "prefix": "" },
|
||||
"repeat-on": { "prefix": "" },
|
||||
"repeat-off": { "prefix": "" }
|
||||
},
|
||||
"arch-update": {
|
||||
"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": "" }
|
||||
},
|
||||
"twmn": {
|
||||
"muted": { "prefix": "" },
|
||||
"unmuted": { "prefix": "" }
|
||||
},
|
||||
"pihole": {
|
||||
"enabled": { "prefix": "" },
|
||||
"disabled": { "prefix": "" }
|
||||
},
|
||||
"vpn": {
|
||||
"prefix": ""
|
||||
},
|
||||
"system": {
|
||||
"prefix": " "
|
||||
},
|
||||
"sun": {
|
||||
"prefix": ""
|
||||
},
|
||||
"rss": {
|
||||
"prefix": ""
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,153 +1,163 @@
|
|||
{
|
||||
"defaults": {
|
||||
"separator": "\ue0b2", "padding": "\u2800",
|
||||
"unknown": { "prefix": "\uf100" }
|
||||
},
|
||||
"date": { "prefix": "\uf2d1" },
|
||||
"time": { "prefix": "\uf3b3" },
|
||||
"datetime": { "prefix": "\uf3b3" },
|
||||
"memory": { "prefix": "\uf389" },
|
||||
"cpu": { "prefix": "\uf4b0" },
|
||||
"disk": { "prefix": "\u26c1" },
|
||||
"dnf": { "prefix": "\uf2be" },
|
||||
"apt": { "prefix": "\uf2be" },
|
||||
"pacman": { "prefix": "\uf2be" },
|
||||
"brightness": { "prefix": "\u263c" },
|
||||
"load": { "prefix": "\uf13d" },
|
||||
"layout": { "prefix": "\uf38c" },
|
||||
"layout-xkb": { "prefix": "\uf38c" },
|
||||
"todo": { "empty": {"prefix": "\uf453" },
|
||||
"items": {"prefix": "\uf454" },
|
||||
"uptime": {"prefix": "\uf4c1" }
|
||||
},
|
||||
"zpool": {
|
||||
"poolread": {"prefix": "\u26c1\uf3d6"},
|
||||
"poolwrite": {"prefix": "\u26c1\uf3d5"},
|
||||
"ONLINE": {"prefix": "\u26c1"},
|
||||
"FAULTED": {"prefix": "\u26c1\uf3bc"},
|
||||
"DEGRADED": {"prefix": "\u26c1\uf3bc"}
|
||||
},
|
||||
"cmus": {
|
||||
"playing": { "prefix": "\uf488" },
|
||||
"paused": { "prefix": "\uf210" },
|
||||
"stopped": { "prefix": "\uf24f" },
|
||||
"prev": { "prefix": "\uf4ab" },
|
||||
"next": { "prefix": "\uf4ad" },
|
||||
"shuffle-on": { "prefix": "\uf4a8" },
|
||||
"shuffle-off": { "prefix": "\uf453" },
|
||||
"repeat-on": { "prefix": "\uf459" },
|
||||
"repeat-off": { "prefix": "\uf30f" }
|
||||
},
|
||||
"gpmdp": {
|
||||
"playing": { "prefix": "\uf488" },
|
||||
"paused": { "prefix": "\uf210" },
|
||||
"stopped": { "prefix": "\uf24f" },
|
||||
"prev": { "prefix": "\uf4ab" },
|
||||
"next": { "prefix": "\uf4ad" }
|
||||
},
|
||||
"pasink": {
|
||||
"muted": { "prefix": "\uf3b9" },
|
||||
"unmuted": { "prefix": "\uf3ba" }
|
||||
},
|
||||
"amixer": {
|
||||
"muted": { "prefix": "\uf3b9" },
|
||||
"unmuted": { "prefix": "\uf3ba" }
|
||||
},
|
||||
"pasource": {
|
||||
"muted": { "prefix": "\uf395" },
|
||||
"unmuted": { "prefix": "\uf2ec" }
|
||||
},
|
||||
"defaults": {
|
||||
"separator": "\ue0b2",
|
||||
"padding": "\u2800",
|
||||
"unknown": { "prefix": "\uf100" }
|
||||
},
|
||||
"date": { "prefix": "\uf2d1" },
|
||||
"time": { "prefix": "\uf3b3" },
|
||||
"datetime": { "prefix": "\uf3b3" },
|
||||
"memory": { "prefix": "\uf389" },
|
||||
"cpu": { "prefix": "\uf4b0" },
|
||||
"disk": { "prefix": "\u26c1" },
|
||||
"dnf": { "prefix": "\uf2be" },
|
||||
"apt": { "prefix": "\uf2be" },
|
||||
"pacman": { "prefix": "\uf2be" },
|
||||
"brightness": { "prefix": "\u263c" },
|
||||
"load": { "prefix": "\uf13d" },
|
||||
"layout": { "prefix": "\uf38c" },
|
||||
"layout-xkb": { "prefix": "\uf38c" },
|
||||
"todo": {
|
||||
"empty": { "prefix": "\uf453" },
|
||||
"items": { "prefix": "\uf454" },
|
||||
"uptime": { "prefix": "\uf4c1" }
|
||||
},
|
||||
"zpool": {
|
||||
"poolread": { "prefix": "\u26c1\uf3d6" },
|
||||
"poolwrite": { "prefix": "\u26c1\uf3d5" },
|
||||
"ONLINE": { "prefix": "\u26c1" },
|
||||
"FAULTED": { "prefix": "\u26c1\uf3bc" },
|
||||
"DEGRADED": { "prefix": "\u26c1\uf3bc" }
|
||||
},
|
||||
"cmus": {
|
||||
"playing": { "prefix": "\uf488" },
|
||||
"paused": { "prefix": "\uf210" },
|
||||
"stopped": { "prefix": "\uf24f" },
|
||||
"prev": { "prefix": "\uf4ab" },
|
||||
"next": { "prefix": "\uf4ad" },
|
||||
"shuffle-on": { "prefix": "\uf4a8" },
|
||||
"shuffle-off": { "prefix": "\uf453" },
|
||||
"repeat-on": { "prefix": "\uf459" },
|
||||
"repeat-off": { "prefix": "\uf30f" }
|
||||
},
|
||||
"gpmdp": {
|
||||
"playing": { "prefix": "\uf488" },
|
||||
"paused": { "prefix": "\uf210" },
|
||||
"stopped": { "prefix": "\uf24f" },
|
||||
"prev": { "prefix": "\uf4ab" },
|
||||
"next": { "prefix": "\uf4ad" }
|
||||
},
|
||||
"pasink": {
|
||||
"muted": { "prefix": "\uf3b9" },
|
||||
"unmuted": { "prefix": "\uf3ba" }
|
||||
},
|
||||
"amixer": {
|
||||
"muted": { "prefix": "\uf3b9" },
|
||||
"unmuted": { "prefix": "\uf3ba" }
|
||||
},
|
||||
"pasource": {
|
||||
"muted": { "prefix": "\uf395" },
|
||||
"unmuted": { "prefix": "\uf2ec" }
|
||||
},
|
||||
"kernel": {
|
||||
"prefix": "\uf17c"
|
||||
},
|
||||
"nic": {
|
||||
"wireless-up": { "prefix": "\uf25c" },
|
||||
"wireless-down": { "prefix": "\uf3d0" },
|
||||
"wired-up": { "prefix": "\uf270" },
|
||||
"wired-down": { "prefix": "\uf271" },
|
||||
"tunnel-up": { "prefix": "\uf133" },
|
||||
"tunnel-down": { "prefix": "\uf306" }
|
||||
},
|
||||
"bluetooth": {
|
||||
"ON": { "prefix": "\uf116" },
|
||||
"OFF": { "prefix": "\uf116" },
|
||||
"?": { "prefix": "\uf116" }
|
||||
"nic": {
|
||||
"wireless-up": { "prefix": "\uf25c" },
|
||||
"wireless-down": { "prefix": "\uf3d0" },
|
||||
"wired-up": { "prefix": "\uf270" },
|
||||
"wired-down": { "prefix": "\uf271" },
|
||||
"tunnel-up": { "prefix": "\uf133" },
|
||||
"tunnel-down": { "prefix": "\uf306" }
|
||||
},
|
||||
"bluetooth": {
|
||||
"ON": { "prefix": "\uf116" },
|
||||
"OFF": { "prefix": "\uf116" },
|
||||
"?": { "prefix": "\uf116" }
|
||||
},
|
||||
"battery": {
|
||||
"charged": { "prefix": "\uf113", "suffix": "\uf493" },
|
||||
"AC": { "suffix": "\uf493" },
|
||||
"charging": {
|
||||
"prefix": ["\uf112", "\uf115", "\uf114", "", "\uf111"],
|
||||
"suffix": "\uf493"
|
||||
},
|
||||
"battery": {
|
||||
"charged": { "prefix": "\uf113", "suffix": "\uf493" },
|
||||
"AC": { "suffix": "\uf493" },
|
||||
"charging": {
|
||||
"prefix": [ "\uf112", "\uf115", "\uf114", "", "\uf111" ],
|
||||
"suffix": "\uf493"
|
||||
},
|
||||
"discharging-10": { "prefix": "\uf112", "suffix": "\uf3bc" },
|
||||
"discharging-25": { "prefix": "\uf115", "suffix": "\uf3e6" },
|
||||
"discharging-50": { "prefix": "\uf115", "suffix": "\uf3e6" },
|
||||
"discharging-80": { "prefix": "\uf114", "suffix": "\uf3e6" },
|
||||
"discharging-100": { "prefix": "\uf113", "suffix": "\uf3e6" },
|
||||
"unknown-10": { "prefix": "\uf112", "suffix": "\uf3bc" },
|
||||
"unknown-25": { "prefix": "\uf115", "suffix": "\uf142" },
|
||||
"unknown-50": { "prefix": "\uf115", "suffix": "\uf142" },
|
||||
"unknown-80": { "prefix": "\uf114", "suffix": "\uf142" },
|
||||
"unknown-100": { "prefix": "\uf113", "suffix": "\uf142" },
|
||||
"unlimited": { "prefix": "\uf402", "suffix": "\uf493" },
|
||||
"estimate": { "prefix": "\uf402" }
|
||||
},
|
||||
"battery_all": {
|
||||
"charged": { "prefix": "\uf113", "suffix": "\uf493" },
|
||||
"AC": { "suffix": "\uf493" },
|
||||
"charging": {
|
||||
"prefix": [ "\uf112", "\uf115", "\uf114", "", "\uf111" ],
|
||||
"suffix": "\uf493"
|
||||
},
|
||||
"discharging-10": { "prefix": "\uf112", "suffix": "\uf3bc" },
|
||||
"discharging-25": { "prefix": "\uf115", "suffix": "\uf3e6" },
|
||||
"discharging-50": { "prefix": "\uf115", "suffix": "\uf3e6" },
|
||||
"discharging-80": { "prefix": "\uf114", "suffix": "\uf3e6" },
|
||||
"discharging-100": { "prefix": "\uf113", "suffix": "\uf3e6" },
|
||||
"unknown-10": { "prefix": "\uf112", "suffix": "\uf3bc" },
|
||||
"unknown-25": { "prefix": "\uf115", "suffix": "\uf142" },
|
||||
"unknown-50": { "prefix": "\uf115", "suffix": "\uf142" },
|
||||
"unknown-80": { "prefix": "\uf114", "suffix": "\uf142" },
|
||||
"unknown-100": { "prefix": "\uf113", "suffix": "\uf142" },
|
||||
"unlimited": { "prefix": "\uf402", "suffix": "\uf493" },
|
||||
"estimate": { "prefix": "\uf402" }
|
||||
},
|
||||
"caffeine": {
|
||||
"activated": {"prefix": "\uf272\u3000\uf354" }, "deactivated": { "prefix": "\uf272\u3000\uf355" }
|
||||
},
|
||||
"xrandr": {
|
||||
"on": { "prefix": "\uf465\u3000\uf354"}, "off": { "prefix": "\uf465\u3000\uf355" }
|
||||
},
|
||||
"redshift": {
|
||||
"day": { "prefix": "\uf4b6" }, "night": { "prefix": "\uf467" }, "transition": { "prefix": "\uf475" }
|
||||
},
|
||||
"discharging-10": { "prefix": "\uf112", "suffix": "\uf3bc" },
|
||||
"discharging-25": { "prefix": "\uf115", "suffix": "\uf3e6" },
|
||||
"discharging-50": { "prefix": "\uf115", "suffix": "\uf3e6" },
|
||||
"discharging-80": { "prefix": "\uf114", "suffix": "\uf3e6" },
|
||||
"discharging-100": { "prefix": "\uf113", "suffix": "\uf3e6" },
|
||||
"unknown-10": { "prefix": "\uf112", "suffix": "\uf3bc" },
|
||||
"unknown-25": { "prefix": "\uf115", "suffix": "\uf142" },
|
||||
"unknown-50": { "prefix": "\uf115", "suffix": "\uf142" },
|
||||
"unknown-80": { "prefix": "\uf114", "suffix": "\uf142" },
|
||||
"unknown-100": { "prefix": "\uf113", "suffix": "\uf142" },
|
||||
"unlimited": { "prefix": "\uf402", "suffix": "\uf493" },
|
||||
"estimate": { "prefix": "\uf402" }
|
||||
},
|
||||
"battery_all": {
|
||||
"charged": { "prefix": "\uf113", "suffix": "\uf493" },
|
||||
"AC": { "suffix": "\uf493" },
|
||||
"charging": {
|
||||
"prefix": ["\uf112", "\uf115", "\uf114", "", "\uf111"],
|
||||
"suffix": "\uf493"
|
||||
},
|
||||
"discharging-10": { "prefix": "\uf112", "suffix": "\uf3bc" },
|
||||
"discharging-25": { "prefix": "\uf115", "suffix": "\uf3e6" },
|
||||
"discharging-50": { "prefix": "\uf115", "suffix": "\uf3e6" },
|
||||
"discharging-80": { "prefix": "\uf114", "suffix": "\uf3e6" },
|
||||
"discharging-100": { "prefix": "\uf113", "suffix": "\uf3e6" },
|
||||
"unknown-10": { "prefix": "\uf112", "suffix": "\uf3bc" },
|
||||
"unknown-25": { "prefix": "\uf115", "suffix": "\uf142" },
|
||||
"unknown-50": { "prefix": "\uf115", "suffix": "\uf142" },
|
||||
"unknown-80": { "prefix": "\uf114", "suffix": "\uf142" },
|
||||
"unknown-100": { "prefix": "\uf113", "suffix": "\uf142" },
|
||||
"unlimited": { "prefix": "\uf402", "suffix": "\uf493" },
|
||||
"estimate": { "prefix": "\uf402" }
|
||||
},
|
||||
"caffeine": {
|
||||
"activated": { "prefix": "\uf272\u3000\uf354" },
|
||||
"deactivated": { "prefix": "\uf272\u3000\uf355" }
|
||||
},
|
||||
"xrandr": {
|
||||
"on": { "prefix": "\uf465\u3000\uf354" },
|
||||
"off": { "prefix": "\uf465\u3000\uf355" }
|
||||
},
|
||||
"redshift": {
|
||||
"day": { "prefix": "\uf4b6" },
|
||||
"night": { "prefix": "\uf467" },
|
||||
"transition": { "prefix": "\uf475" }
|
||||
},
|
||||
"sensors": {
|
||||
"prefix": "\uf3b6"
|
||||
},
|
||||
"traffic":{
|
||||
"rx": { "prefix": "\uf365" },
|
||||
"tx": { "prefix": "\uf35f" }
|
||||
"traffic": {
|
||||
"rx": { "prefix": "\uf365" },
|
||||
"tx": { "prefix": "\uf35f" }
|
||||
},
|
||||
"network_traffic": {
|
||||
"rx": { "prefix": "\uf365" },
|
||||
"tx": { "prefix": "\uf35f" }
|
||||
},
|
||||
"mpd": {
|
||||
"playing": { "prefix": "\uf488" },
|
||||
"paused": { "prefix": "\uf210" },
|
||||
"stopped": { "prefix": "\uf24f" },
|
||||
"prev": { "prefix": "\uf4ab" },
|
||||
"next": { "prefix": "\uf4ad" },
|
||||
"shuffle-on": { "prefix": "\uf4a8" },
|
||||
"shuffle-off": { "prefix": "\uf453" },
|
||||
"repeat-on": { "prefix": "\uf459" },
|
||||
"repeat-off": { "prefix": "\uf30f" }
|
||||
},
|
||||
"playing": { "prefix": "\uf488" },
|
||||
"paused": { "prefix": "\uf210" },
|
||||
"stopped": { "prefix": "\uf24f" },
|
||||
"prev": { "prefix": "\uf4ab" },
|
||||
"next": { "prefix": "\uf4ad" },
|
||||
"shuffle-on": { "prefix": "\uf4a8" },
|
||||
"shuffle-off": { "prefix": "\uf453" },
|
||||
"repeat-on": { "prefix": "\uf459" },
|
||||
"repeat-off": { "prefix": "\uf30f" }
|
||||
},
|
||||
"github": {
|
||||
"prefix": "\uf233"
|
||||
"prefix": "\uf233"
|
||||
},
|
||||
"spotify": {
|
||||
"prefix": "\uf305"
|
||||
"prefix": "\uf305"
|
||||
},
|
||||
"publicip": {
|
||||
"prefix": "\uf268"
|
||||
"prefix": "\uf268"
|
||||
},
|
||||
"weather": {
|
||||
"clouds": { "prefix": "\uf12b" },
|
||||
|
@ -157,11 +167,23 @@
|
|||
"thunder": { "prefix": "\uf4bd" }
|
||||
},
|
||||
"taskwarrior": {
|
||||
"prefix": "\uf454"
|
||||
"prefix": "\uf454"
|
||||
},
|
||||
"dunst": {
|
||||
"muted": { "prefix": "\uf39a"},
|
||||
"unmuted": { "prefix": "\uf39b" }
|
||||
}
|
||||
|
||||
"muted": { "prefix": "\uf39a" },
|
||||
"unmuted": { "prefix": "\uf39b" }
|
||||
},
|
||||
"twmn": {
|
||||
"muted": { "prefix": "\uf1f6" },
|
||||
"unmuted": { "prefix": "\uf0f3" }
|
||||
},
|
||||
"system": {
|
||||
"prefix": " \uf2a9 "
|
||||
},
|
||||
"sun": {
|
||||
"prefix": "\uf3b0"
|
||||
},
|
||||
"rss": {
|
||||
"prefix": "\uf1ea"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue