diff --git a/.travis.yml b/.travis.yml index 0e198ad..61e1ff9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ python: - "3.5" - "3.6" install: + - pip install i3ipc - pip install psutil - pip install netifaces - pip install -U coverage==4.3 diff --git a/PKGBUILD b/PKGBUILD new file mode 100644 index 0000000..15d0e43 --- /dev/null +++ b/PKGBUILD @@ -0,0 +1,37 @@ +# Maintainer: Tobias Witek +# Contributor: Daniel M. Capella + +pkgname=bumblebee-status +pkgver=1.4.2 +pkgrel=1 +pkgdesc='Modular, theme-able status line generator for the i3 window manager' +arch=('any') +url=https://github.com/tobi-wan-kenobi/bumblebee-status +license=('MIT') +depends=('python' 'python-netifaces' 'python-psutil' 'python-requests') +optdepends=('xorg-xbacklight: to display a displays brightness' + 'xorg-xset: enable/disable automatic screen locking' + 'libnotify: enable/disable automatic screen locking' + 'dnf: display DNF package update information' + 'xorg-setxkbmap: display/change the current keyboard layout' + 'redshift: display the redshifts current color' + 'pulseaudio: control pulseaudio sink/sources' + 'xorg-xrandr: enable/disable screen outputs' + 'pacman: display current status of pacman' + 'iputils: display a ping' + 'i3ipc-python: display titlebar' + 'fakeroot: dependency of the pacman module') +source=("$pkgname-$pkgver.tar.gz::$url/archive/v$pkgver.tar.gz") +sha512sums=('3a66fc469dd3b081337c9e213a1b2262f25f30977ee6ef65b9fa5a8b6aa341637832d1a5dbb74e30d68e2824e0d19d7a911eb3390dc6062707a552f429b483e8') + +package() { + install -d "$pkgdir"/usr/bin \ + "$pkgdir"/usr/share/$pkgname/{bumblebee/modules,themes/icons} + 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 + + install -Dm644 LICENSE "$pkgdir"/usr/share/licenses/$pkgname/LICENSE +} diff --git a/README.md b/README.md index 8c8b6c6..aaf0e30 100644 --- a/README.md +++ b/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, 21 of the modules are from various contributors (!), and only 16 from myself.** +**Many, many thanks to all contributors! As of now, 22 of the modules are from various contributors (!), and only 16 from myself.** bumblebee-status is a modular, theme-able status line generator for the [i3 window manager](https://i3wm.org/). @@ -20,6 +20,8 @@ I hope you like it and appreciate any kind of feedback: Bug reports, Feature req Thanks a lot! +Required i3wm version: 4.12+ (in earlier versions, blocks won't have background colors) + Supported Python versions: 2.7, 3.3, 3.4, 3.5, 3.6 Explicitly unsupported Python versions: 3.2 (missing unicode literals) @@ -108,6 +110,7 @@ Modules and commandline utilities are only required for modules, the core itself * requests (for the modules 'weather', 'github', 'getcrypto', 'stock') * power (for the module 'battery') * dbus (for the module 'spotify') +* i3rpc (for the module 'title') # Required commandline utilities diff --git a/bumblebee-status b/bumblebee-status index f28dc7c..e3fe4ca 100755 --- a/bumblebee-status +++ b/bumblebee-status @@ -10,6 +10,13 @@ import bumblebee.output import bumblebee.input import bumblebee.modules.error +try: + reload(sys) + sys.setdefaultencoding('UTF8') +except Exception: + pass + + def main(): config = bumblebee.config.Config(sys.argv[1:]) diff --git a/bumblebee/modules/cmus.py b/bumblebee/modules/cmus.py index 62fbfaf..2db4c0c 100644 --- a/bumblebee/modules/cmus.py +++ b/bumblebee/modules/cmus.py @@ -1,5 +1,4 @@ # pylint: disable=C0111,R0903 -# -*- coding: utf-8 -*- """Displays information about the current song in cmus. @@ -69,10 +68,6 @@ class Module(bumblebee.engine.Module): return returns.get(widget.name, self._status) def _eval_line(self, line): - # not a typo, use decode detection to see whether we are - # dealing with Python2 or Python3 - if hasattr(line, "decode"): - line = line.encode("utf-8", "replace") name, key, value = (line.split(" ", 2) + [None, None])[:3] if name == "status": diff --git a/bumblebee/modules/currency.py b/bumblebee/modules/currency.py index 488eebb..81d533e 100644 --- a/bumblebee/modules/currency.py +++ b/bumblebee/modules/currency.py @@ -8,6 +8,13 @@ Requires the following python packages: Parameters: * currency.interval: Interval in minutes between updates, default is 1. + * currency.source: Source currency (defaults to "GBP") + * currency.destination: Comma-separated list of destination currencies (defaults to "USD,EUR") + * currency.sourceformat: String format for source formatting; Defaults to "{}: {}" and has two variables, + the base symbol and the rate list + * currency.destinationdelimiter: Delimiter used for separating individual rates (defaults to "|") + +Note: source and destination names right now must correspond to the names used by the API of http://fixer.io """ import bumblebee.input @@ -21,40 +28,43 @@ try: except ImportError: pass +SYMBOL = { + "GBP": u"£", "EUR": u"€", "USD": u"$" +} + class Module(bumblebee.engine.Module): def __init__(self, engine, config): super(Module, self).__init__(engine, config, bumblebee.output.Widget(full_text=self.price) ) - self._price = "-" - self._interval = int(self.parameter("interval", "1")) + self._data = {} + self._interval = int(self.parameter("interval", 1)) + self._base = self.parameter("source", "GBP") + self._symbols = self.parameter("destination", "USD,EUR") self._nextcheck = 0 - self._valid = False def price(self, widget): - if not self._valid: - return u"?" - return self._price + if self._data == {}: + return "?" + + rates = [] + for sym in self._data["rates"]: + rates.append(u"{}{}".format(self._data["rates"][sym], SYMBOL[sym] if sym in SYMBOL else sym)) + + basefmt = u"{}".format(self.parameter("sourceformat", "{}: {}")) + ratefmt = u"{}".format(self.parameter("destinationdelimiter", "|")) + + return basefmt.format(SYMBOL[self._base] if self._base in SYMBOL else self._base, ratefmt.join(rates)) def update(self, widgets): timestamp = int(time.time()) if self._nextcheck < int(time.time()): + self._data = {} + self._nextcheck = int(time.time()) + self._interval*60 + url = "http://api.fixer.io/latest?symbols={}&base={}".format(self._symbols, self._base) try: - self._nextcheck = int(time.time()) + self._interval*60 - price_url = "http://api.fixer.io/latest?symbols=USD,EUR&base=GBP" - try: - price_json = json.loads( requests.get(price_url).text ) - gbpeur = str(price_json['rates']['EUR']) - gbpusd = str(price_json['rates']['USD']) - except ValueError: - gbpeur = "-" - gbpusd = "-" - - self._price = u"£/€ " + gbpeur + u" | £/$ " + gbpusd - self._valid = True - except RequestException: - self._price = u"£/€ - | £/$ -" - self._valid = True + self._data = json.loads(requests.get(url).text) + except Exception: + pass # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 - diff --git a/bumblebee/modules/github.py b/bumblebee/modules/github.py index bbd3bd8..d9a5576 100644 --- a/bumblebee/modules/github.py +++ b/bumblebee/modules/github.py @@ -49,7 +49,7 @@ class Module(bumblebee.engine.Module): url = "https://api.github.com/notifications" while True: notifications = self._requests.get(url) - self._count += len(filter(lambda notification: notification.get("unread", False), notifications.json())) + self._count += len(list(filter(lambda notification: notification['unread'], notifications.json()))) next_link = notifications.links.get('next') if next_link is not None: url = next_link.get('url') diff --git a/bumblebee/modules/layout.py b/bumblebee/modules/layout.py index d9daadb..49c59a4 100644 --- a/bumblebee/modules/layout.py +++ b/bumblebee/modules/layout.py @@ -4,9 +4,6 @@ Requires the following executable: * setxkbmap - -Parameters: - * layout.lang: pipe-separated list of languages to cycle through (e.g. us|rs|de). Default: en """ import bumblebee.util @@ -17,58 +14,59 @@ 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.layout) + bumblebee.output.Widget(full_text=self.current_layout) ) - self._languages = self.parameter("lang", "us").split("|") - self._idx = 0 - engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE, cmd=self._next_keymap) engine.input.register_callback(self, button=bumblebee.input.RIGHT_MOUSE, cmd=self._prev_keymap) def _next_keymap(self, event): - self._idx = (self._idx + 1) % len(self._languages) - self._set_keymap() + self._set_keymap(1) def _prev_keymap(self, event): - self._idx = self._idx - 1 if self._idx > 0 else len(self._languages) - 1 - self._set_keymap() + self._set_keymap(-1) + + def _set_keymap(self, rotation): + layouts = self.get_layouts() + if len(layouts) == 1: return # nothing to do + layouts = layouts[rotation:] + layouts[:rotation] + + layout_list = [] + variant_list = [] + for l in layouts: + tmp = l.split(":") + layout_list.append(tmp[0]) + variant_list.append(tmp[1] if len(tmp) > 1 else "") - def _set_keymap(self): - tmp = self._languages[self._idx].split(":") - layout = tmp[0] - variant = "" - if len(tmp) > 1: - variant = "-variant {}".format(tmp[1]) try: - bumblebee.util.execute("setxkbmap -layout {} {}".format(layout, variant)) + bumblebee.util.execute("setxkbmap -layout {} -variant {}".format(",".join(layout_list), ",".join(variant_list))) except RuntimeError: pass - def layout(self, widget): + def get_layouts(self): try: res = bumblebee.util.execute("setxkbmap -query") except RuntimeError: - return "n/a" - layout = "" - variant = None + return ["n/a"] + layouts = [] + variants = [] for line in res.split("\n"): - if not line: - continue + if not line: continue if "layout" in line: - layout = line.split(":")[1].strip() + layouts = line.split(":")[1].strip().split(",") if "variant" in line: - variant = line.split(":")[1].strip() - if variant: - layout += ":" + variant + variants = line.split(":")[1].strip().split(",") - if layout in self._languages: - self._idx = self._languages.index(layout) - else: - self._languages.append(layout) - self._idx = len(self._languages) - 1 + result = [] + for idx, layout in enumerate(layouts): + if len(variants) > idx and variants[idx]: + layout = "{}:{}".format(layout, variants[idx]) + result.append(layout) + return result if len(result) > 0 else ["n/a"] - return layout + def current_layout(self, widget): + layouts = self.get_layouts() + return layouts[0] # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/modules/memory.py b/bumblebee/modules/memory.py index e92fc10..752a8a0 100644 --- a/bumblebee/modules/memory.py +++ b/bumblebee/modules/memory.py @@ -9,22 +9,24 @@ Parameters: * memory.usedonly: Only show the amount of RAM in use (defaults to False). Same as memory.format="{used}" """ -try: - import psutil -except ImportError: - pass +import re import bumblebee.util import bumblebee.input import bumblebee.output import bumblebee.engine +class Container(object): + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + class Module(bumblebee.engine.Module): def __init__(self, engine, config): super(Module, self).__init__(engine, config, bumblebee.output.Widget(full_text=self.memory_usage) ) - self._mem = psutil.virtual_memory() + self.update(None) + engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE, cmd="gnome-system-monitor") @@ -36,18 +38,30 @@ class Module(bumblebee.engine.Module): return self.parameter("format", "{used}/{total} ({percent:05.02f}%)") def memory_usage(self, widget): - used = bumblebee.util.bytefmt(self._mem.total - self._mem.available) - total = bumblebee.util.bytefmt(self._mem.total) - - return self._format.format(used=used, total=total, percent=self._mem.percent) + return self._format.format(**self._mem) def update(self, widgets): - self._mem = psutil.virtual_memory() + data = {} + with open("/proc/meminfo", "r") as f: + for line in f: + tmp = re.split(r"[:\s]+", line) + value = int(tmp[1]) + if tmp[2] == "kB": value = value*1024 + if tmp[2] == "mB": value = value*1024*1024 + if tmp[2] == "gB": value = value*1024*1024*1024 + data[tmp[0]] = value + self._mem = { + "total": bumblebee.util.bytefmt(data["MemTotal"]), + "available": bumblebee.util.bytefmt(data["MemAvailable"]), + "free": bumblebee.util.bytefmt(data["MemFree"]), + "used": bumblebee.util.bytefmt(data["MemTotal"] - data["MemFree"] - data["Buffers"] - data["Cached"] - data["Slab"]), + "percent": (float(data["MemTotal"] - data["MemAvailable"])/data["MemTotal"])*100 + } def state(self, widget): - if self._mem.percent > float(self.parameter("critical", 90)): + if self._mem["percent"] > float(self.parameter("critical", 90)): return "critical" - if self._mem.percent > float(self.parameter("warning", 80)): + if self._mem["percent"] > float(self.parameter("warning", 80)): return "warning" return None diff --git a/bumblebee/modules/redshift.py b/bumblebee/modules/redshift.py index 2fd1dfb..5dce616 100644 --- a/bumblebee/modules/redshift.py +++ b/bumblebee/modules/redshift.py @@ -38,7 +38,9 @@ class Module(bumblebee.engine.Module): else: self._state = "transition" transition = " ".join(line.split(" ")[2:]) - self._text = "{} {}".format(temp, transition) + self._text = temp + if transition: + self._text = "{} {}".format(temp, transition) def state(self, widget): return self._state diff --git a/bumblebee/modules/title.py b/bumblebee/modules/title.py new file mode 100644 index 0000000..1b5ac75 --- /dev/null +++ b/bumblebee/modules/title.py @@ -0,0 +1,67 @@ +# pylint: disable=C0111,R0903 + +"""Displays focused i3 window title. + +Requirements: + * i3ipc + +Parameters: + * title.max : Maximum character length for title before truncating. Defaults to 64. + * title.placeholder : Placeholder text to be placed if title was truncated. Defaults to "...". + * title.scroll : Boolean flag for scrolling title. Defaults to False +""" + +try: + import i3ipc +except ImportError: + pass + +import bumblebee.util +import bumblebee.input +import bumblebee.output +import bumblebee.engine + +from bumblebee.output import scrollable + +class Module(bumblebee.engine.Module): + """Window title module.""" + + def __init__(self, engine, config): + super(Module, self).__init__( + engine, + config, + bumblebee.output.Widget(full_text=self.get_title) + ) + try: + self._i3 = i3ipc.Connection() + self._full_title = self._i3.get_tree().find_focused().name + except Exception: + self._full_title = "n/a" + + def get_title(self, widget): + if bumblebee.util.asbool(self.parameter("scroll", False)): + return self.scrolling_focused_title(widget) + else: + return self.focused_title(widget) + + def focused_title(self, widget): + title = self._full_title[0:self.parameter("max", 64)] + placeholder = self.parameter("placeholder", "...") + if title != self._full_title: + title = self._full_title[0:self.parameter("max", 64) - len(placeholder)] + title = "{}{}".format(title, placeholder) + + return title + + @scrollable + def scrolling_focused_title(self, widget): + return self._full_title + + def update(self, widgets): + """Update current title.""" + try: + self._full_title = self._i3.get_tree().find_focused().name + except Exception: + self._full_title = "n/a" + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/modules/traffic.py b/bumblebee/modules/traffic.py index 500eaa5..60d5dd7 100644 --- a/bumblebee/modules/traffic.py +++ b/bumblebee/modules/traffic.py @@ -5,6 +5,7 @@ Parameters: * traffic.exclude: Comma-separated list of interface prefixes to exclude (defaults to "lo,virbr,docker,vboxnet,veth") * traffic.states: Comma-separated list of states to show (prefix with "^" to invert - i.e. ^down -> show all devices that are not in state down) + * traffic.showname: If set to False, hide network interface name (defaults to True) """ import re @@ -23,7 +24,7 @@ class Module(bumblebee.engine.Module): self._exclude = tuple(filter(len, self.parameter("exclude", "lo,virbr,docker,vboxnet,veth").split(","))) self._status = "" - self._showname = self.parameter("showname", "True") + self._showname = bumblebee.util.asbool(self.parameter("showname", True)) self._prev = {} self._states = {} self._states["include"] = [] @@ -86,13 +87,13 @@ class Module(bumblebee.engine.Module): } name = "traffic-{}".format(interface) - - if self._showname != "False": - self.create_widget(widgets, name, interface) + + if self._showname: + self.create_widget(widgets, name, interface) for direction in ["rx", "tx"]: name = "traffic.{}-{}".format(direction, interface) - widget = self.create_widget(widgets, name, attributes={"theme.minwidth": "100.00MB"}) + 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)) widget.full_text(speed) diff --git a/bumblebee/theme.py b/bumblebee/theme.py index e16fbb6..647a823 100644 --- a/bumblebee/theme.py +++ b/bumblebee/theme.py @@ -33,9 +33,9 @@ class Theme(object): def _init(self, data): """Initialize theme from data structure""" + self._theme = data for iconset in data.get("icons", []): self._merge(data, self._load_icons(iconset)) - self._theme = data self._defaults = data.get("defaults", {}) self._cycles = self._theme.get("cycle", []) self.reset() @@ -174,7 +174,8 @@ class Theme(object): if key in target and isinstance(target[key], dict): self._merge(target[key], value) else: - target[key] = copy.deepcopy(value) + if not key in target: + target[key] = copy.deepcopy(value) return target # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/screenshots/title.png b/screenshots/title.png new file mode 100644 index 0000000..1089a0b Binary files /dev/null and b/screenshots/title.png differ diff --git a/tests/modules/test_memory.py b/tests/modules/test_memory.py deleted file mode 100644 index 3f98ca9..0000000 --- a/tests/modules/test_memory.py +++ /dev/null @@ -1,61 +0,0 @@ -# pylint: disable=C0103,C0111 - -import mock -import unittest - -import tests.mocks as mocks - -from bumblebee.input import LEFT_MOUSE -from bumblebee.modules.memory import Module - -class VirtualMemory(object): - def __init__(self, percent): - self.percent = percent - -class TestMemoryModule(unittest.TestCase): - def setUp(self): - mocks.setup_test(self, Module) - self._psutil = mock.patch("bumblebee.modules.memory.psutil") - self.psutil = self._psutil.start() - - def tearDown(self): - self._psutil.stop() - mocks.teardown_test(self) - - def test_leftclick(self): - mocks.mouseEvent(stdin=self.stdin, button=LEFT_MOUSE, inp=self.input, module=self.module) - self.popen.assert_call("gnome-system-monitor") - - def test_warning(self): - self.config.set("memory.critical", "80") - self.config.set("memory.warning", "70") - self.psutil.virtual_memory.return_value = VirtualMemory(75) - self.module.update_all() - self.assertTrue("warning" in self.module.state(self.anyWidget)) - - def test_critical(self): - self.config.set("memory.critical", "80") - self.config.set("memory.warning", "70") - self.psutil.virtual_memory.return_value = VirtualMemory(81) - self.module.update_all() - self.assertTrue("critical" in self.module.state(self.anyWidget)) - - def test_format(self): - self.config.set("memory.format", "memory used: {used}") - rv = VirtualMemory(50) - rv.total = 1000 - rv.available = 500 - self.psutil.virtual_memory.return_value = rv - self.module.update_all() - self.assertEquals("memory used: 500.00B", self.module.memory_usage(self.anyWidget)) - - def test_usage(self): - rv = VirtualMemory(50) - rv.total = 1000 - rv.available = 500 - self.psutil.virtual_memory.return_value = rv - self.module.update_all() - self.assertEquals("500.00B/1000.00B (50.00%)", self.module.memory_usage(self.anyWidget)) - self.assertEquals(None, self.module.state(self.anyWidget)) - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/themes/solarized-dark-awesome.json b/themes/solarized-dark-awesome.json new file mode 100644 index 0000000..1ec6dfe --- /dev/null +++ b/themes/solarized-dark-awesome.json @@ -0,0 +1,38 @@ +{ + "icons": [ "awesome-fonts" ], + "defaults": { + "separator-block-width": 0, + "separator": "", + "warning": { + "fg": "#002b36", + "bg": "#b58900" + }, + "critical": { + "fg": "#002b36", + "bg": "#dc322f" + }, + "fg": "#93a1a1", "bg": "#002b36" + }, + "dnf": { + "good": { + "fg": "#002b36", + "bg": "#859900" + } + }, + "pacman": { + "good": { + "fg": "#002b36", + "bg": "#859900" + } + }, + "battery": { + "charged": { + "fg": "#002b36", + "bg": "#859900" + }, + "AC": { + "fg": "#002b36", + "bg": "#859900" + } + } +}