Merge branch 'main' into pactl_revert

This commit is contained in:
Bernhard B 2022-03-07 20:52:31 +01:00
commit 0bf91c2f15
129 changed files with 3295 additions and 432 deletions

70
.github/workflows/codeql-analysis.yml vendored Normal file
View file

@ -0,0 +1,70 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '31 0 * * 4'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

2
.gitignore vendored
View file

@ -1,3 +1,5 @@
*.o
# Vim swap files # Vim swap files
*swp *swp
*~ *~

6
.readthedocs.yaml Normal file
View file

@ -0,0 +1,6 @@
version: 2
python:
install:
- requirements: docs/requirements.txt

View file

@ -1,26 +1,30 @@
sudo: false os: linux
language: python language: python
env:
global:
- CC_TEST_REPORTER_ID=40cb00907f7a10e04868e856570bb997ab9c42fd3b63d980f2b2269433195fdf
python: python:
- "3.4"
- "3.5"
- "3.6" - "3.6"
- "3.7" - "3.7"
before_install: - "3.8"
- sudo apt-get -qq update - "3.9"
before_script:
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
- chmod +x ./cc-test-reporter
- ./cc-test-reporter before-build
addons:
apt:
packages:
libdbus-1-dev
libgit2-dev
libvirt-dev
taskwarrior
install: install:
- sudo apt-get install python-dbus - pip install -U coverage pytest pytest-mock freezegun
- pip install -U coverage==4.3 pytest pytest-mock - pip install 'pygit2<1' 'libvirt-python<6.3' 'feedparser<6' || true
- pip install codeclimate-test-reporter - pip install $(cat requirements/modules/*.txt | cut -d ' ' -f 1 | sort -u)
- pip install i3-py Pillow Babel DateTime python-dateutil
- pip install docker feedparser i3ipc
- pip install netifaces power
- pip install psutil pytz
- pip install requests simplejson
- pip install suntime
- pip install tzlocal
script: script:
- coverage run --source=. -m pytest tests -v - coverage run --source=. -m pytest tests -v
- CODECLIMATE_REPO_TOKEN=40cb00907f7a10e04868e856570bb997ab9c42fd3b63d980f2b2269433195fdf codeclimate-test-reporter after_script:
addons: - coverage xml
code_climate: - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
repo_token: 40cb00907f7a10e04868e856570bb997ab9c42fd3b63d980f2b2269433195fdf

View file

@ -1,6 +1,6 @@
# bumblebee-status # bumblebee-status
[![Build Status](https://travis-ci.org/tobi-wan-kenobi/bumblebee-status.svg?branch=main)](https://travis-ci.org/tobi-wan-kenobi/bumblebee-status) [![Build Status](https://app.travis-ci.com/tobi-wan-kenobi/bumblebee-status.svg?branch=main)](https://app.travis-ci.com/tobi-wan-kenobi/bumblebee-status)
[![Documentation Status](https://readthedocs.org/projects/bumblebee-status/badge/?version=main)](https://bumblebee-status.readthedocs.io/en/main/?badge=main) [![Documentation Status](https://readthedocs.org/projects/bumblebee-status/badge/?version=main)](https://bumblebee-status.readthedocs.io/en/main/?badge=main)
![AUR version (release)](https://img.shields.io/aur/version/bumblebee-status) ![AUR version (release)](https://img.shields.io/aur/version/bumblebee-status)
![AUR version (git)](https://img.shields.io/aur/version/bumblebee-status-git) ![AUR version (git)](https://img.shields.io/aur/version/bumblebee-status-git)
@ -8,6 +8,8 @@
[![Code Climate](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/gpa.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status) [![Code Climate](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/gpa.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status)
[![Test Coverage](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/coverage.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/coverage) [![Test Coverage](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/coverage.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/coverage)
[![Issue Count](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/issue_count.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status) [![Issue Count](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/issue_count.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status)
[![CodeQL](https://github.com/tobi-wan-kenobi/bumblebee-status/actions/workflows/codeql-analysis.yml/badge.svg?branch=main)](https://github.com/tobi-wan-kenobi/bumblebee-status/actions/workflows/codeql-analysis.yml)
![License](https://img.shields.io/github/license/tobi-wan-kenobi/bumblebee-status)
**Many, many thanks to all contributors! I am still amazed by and deeply grateful for how many PRs this project gets.** **Many, many thanks to all contributors! I am still amazed by and deeply grateful for how many PRs this project gets.**
@ -28,16 +30,14 @@ Thanks a lot!
Required i3wm version: 4.12+ (in earlier versions, blocks won't have background colors) Required i3wm version: 4.12+ (in earlier versions, blocks won't have background colors)
Supported Python versions: 3.4, 3.5, 3.6, 3.7, 3.8 Supported Python versions: 3.4, 3.5, 3.6, 3.7, 3.8, 3.9
Supported FontAwesome version: 4 (free version of 5 doesn't include some of the icons) Supported FontAwesome version: 4 (free version of 5 doesn't include some of the icons)
--- ---
**NOTE** ***NOTE***
The default branch for this project is `main` - I'm keeping `master` around for backwards compatibility (I do not want to break anybody's setup), but the default branch is now `main`! The default branch for this project is `main`. If you are curious why: [ZDNet:github-master-alternative](https://www.zdnet.com/article/github-to-replace-master-with-alternative-term-to-avoid-slavery-references/)
If you are curious why: [ZDNet:github-master-alternative](https://www.zdnet.com/article/github-to-replace-master-with-alternative-term-to-avoid-slavery-references/)
--- ---
@ -76,10 +76,16 @@ makepkg -sicr
pip install --user bumblebee-status pip install --user bumblebee-status
``` ```
There is also a SlackBuild available here: [slackbuilds:bumblebee-status](http://slackbuilds.org/repository/14.2/desktop/bumblebee-status/) - many thanks to [@Tonus1](https://github.com/Tonus1)!
An ebuild, for Gentoo Linux, is available on [gallifrey overlay](https://github.com/fedeliallalinea/gallifrey/tree/master/x11-misc/bumblebee-status). Instructions for adding the overlay can be found [here](https://github.com/fedeliallalinea/gallifrey/blob/master/README.md).
# Dependencies # Dependencies
[Available modules](https://bumblebee-status.readthedocs.io/en/main/modules.html) lists the dependencies (Python modules and external executables) [Available modules](https://bumblebee-status.readthedocs.io/en/main/modules.html) lists the dependencies (Python modules and external executables)
for each module. If you are not using a module, you don't need the dependencies. for each module. If you are not using a module, you don't need the dependencies.
Some themes (e.g. all powerline themes) require Font Awesome http://fontawesome.io/ and a powerline-compatible font (powerline-fonts) https://github.com/powerline/fonts
# Usage # Usage
## Normal usage ## Normal usage
In your i3wm configuration, modify the *status_command* for your i3bar like this: In your i3wm configuration, modify the *status_command* for your i3bar like this:

BIN
bin/get-kbd-layout Executable file

Binary file not shown.

View file

@ -12,6 +12,7 @@ button = {
"right-mouse": 3, "right-mouse": 3,
"wheel-up": 4, "wheel-up": 4,
"wheel-down": 5, "wheel-down": 5,
"update": -1,
} }
@ -20,7 +21,7 @@ def main():
parser.add_argument( parser.add_argument(
"-b", "-b",
"--button", "--button",
choices=["left-mouse", "right-mouse", "middle-mouse", "wheel-up", "wheel-down"], choices=["left-mouse", "right-mouse", "middle-mouse", "wheel-up", "wheel-down", "update"],
help="button to emulate", help="button to emulate",
default="left-mouse", default="left-mouse",
) )

View file

@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
import os import os
import sys import sys
@ -6,7 +6,6 @@ import json
import time import time
import signal import signal
import socket import socket
import select
import logging import logging
import threading import threading
@ -39,41 +38,40 @@ class CommandSocket(object):
self.__socket.close() self.__socket.close()
os.unlink(self.__name) os.unlink(self.__name)
def process_event(event_line, config, update_lock):
modules = {}
try:
event = json.loads(event_line)
core.input.trigger(event)
if "name" in event:
modules[event["name"]] = True
except ValueError:
pass
def handle_input(output, update_lock): delay = float(config.get("engine.input_delay", 0.0))
if delay > 0:
time.sleep(delay)
if update_lock.acquire(blocking=False) == True:
core.event.trigger("update", modules.keys(), force=True)
core.event.trigger("draw")
update_lock.release()
def handle_commands(config, update_lock):
with CommandSocket() as cmdsocket: with CommandSocket() as cmdsocket:
poll = select.poll()
poll.register(sys.stdin.fileno(), select.POLLIN)
poll.register(cmdsocket, select.POLLIN)
while True: while True:
events = poll.poll() tmp, _ = cmdsocket.accept()
line = tmp.recv(4096).decode()
tmp.close()
logging.debug("socket event {}".format(line))
process_event(line, config, update_lock)
modules = {}
for fileno, event in events:
if fileno == cmdsocket.fileno():
tmp, _ = cmdsocket.accept()
line = tmp.recv(4096).decode()
tmp.close()
logging.debug("socket event {}".format(line))
else:
line = "["
while line.startswith("["):
line = sys.stdin.readline().strip(",").strip()
logging.info("input event: {}".format(line))
try:
event = json.loads(line)
core.input.trigger(event)
if "name" in event:
modules[event["name"]] = True
except ValueError:
pass
update_lock.acquire()
core.event.trigger("update", modules.keys())
core.event.trigger("draw")
update_lock.release()
poll.unregister(sys.stdin.fileno()) def handle_events(config, update_lock):
while True:
line = sys.stdin.readline().strip(",").strip()
if line == "[": continue
logging.info("input event: {}".format(line))
process_event(line, config, update_lock)
def main(): def main():
@ -100,9 +98,13 @@ def main():
core.input.register(None, core.input.WHEEL_DOWN, "i3-msg workspace next_on_output") core.input.register(None, core.input.WHEEL_DOWN, "i3-msg workspace next_on_output")
update_lock = threading.Lock() update_lock = threading.Lock()
input_thread = threading.Thread(target=handle_input, args=(output, update_lock, )) event_thread = threading.Thread(target=handle_events, args=(config, update_lock, ))
input_thread.daemon = True event_thread.daemon = True
input_thread.start() event_thread.start()
cmd_thread = threading.Thread(target=handle_commands, args=(config, update_lock, ))
cmd_thread.daemon = True
cmd_thread.start()
def sig_USR1_handler(signum,stack): def sig_USR1_handler(signum,stack):
if update_lock.acquire(blocking=False) == True: if update_lock.acquire(blocking=False) == True:

View file

@ -147,6 +147,13 @@ class Config(util.store.Store):
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="bumblebee-status is a modular, theme-able status line generator for the i3 window manager. https://github.com/tobi-wan-kenobi/bumblebee-status/wiki" description="bumblebee-status is a modular, theme-able status line generator for the i3 window manager. https://github.com/tobi-wan-kenobi/bumblebee-status/wiki"
) )
parser.add_argument(
"-c",
"--config-file",
action="store",
default=None,
help="Specify a configuration file to use"
)
parser.add_argument( parser.add_argument(
"-m", "--modules", nargs="+", action="append", default=[], help=MODULE_HELP "-m", "--modules", nargs="+", action="append", default=[], help=MODULE_HELP
) )
@ -172,6 +179,13 @@ class Config(util.store.Store):
default=[], default=[],
help="Specify a list of modules to hide when not in warning/error state", help="Specify a list of modules to hide when not in warning/error state",
) )
parser.add_argument(
"-e",
"--errorhide",
nargs="+",
default=[],
help="Specify a list of modules that are hidden when in state error"
)
parser.add_argument( parser.add_argument(
"-d", "--debug", action="store_true", help="Add debug fields to i3 output" "-d", "--debug", action="store_true", help="Add debug fields to i3 output"
) )
@ -196,13 +210,18 @@ class Config(util.store.Store):
self.__args = parser.parse_args(args) self.__args = parser.parse_args(args)
for cfg in [ if self.__args.config_file:
"~/.bumblebee-status.conf", cfg = self.__args.config_file
"~/.config/bumblebee-status.conf",
"~/.config/bumblebee-status/config",
]:
cfg = os.path.expanduser(cfg) cfg = os.path.expanduser(cfg)
self.load_config(cfg) self.load_config(cfg)
else:
for cfg in [
"~/.bumblebee-status.conf",
"~/.config/bumblebee-status.conf",
"~/.config/bumblebee-status/config",
]:
cfg = os.path.expanduser(cfg)
self.load_config(cfg)
parameters = [item for sub in self.__args.parameters for item in sub] parameters = [item for sub in self.__args.parameters for item in sub]
for param in parameters: for param in parameters:
@ -302,14 +321,21 @@ class Config(util.store.Store):
def iconset(self): def iconset(self):
return self.__args.iconset return self.__args.iconset
"""Returns which modules should be hidden if their state is not warning/critical """Returns whether a module should be hidden if their state is not warning/critical
:return: list of modules to hide automatically :return: True if module should be hidden automatically, False otherwise
:rtype: list of strings :rtype: bool
""" """
def autohide(self, name): def autohide(self, name):
return name in self.__args.autohide return name in self.__args.autohide or name in util.format.aslist(self.get("autohide", []))
"""Returns which modules should be hidden if they are in state error
:return: returns True if name should be hidden, False otherwise
:rtype: bool
"""
def errorhide(self, name):
return name in self.__args.errorhide
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -8,6 +8,13 @@ def register(event, callback, *args, **kwargs):
__callbacks.setdefault(event, []).append(cb) __callbacks.setdefault(event, []).append(cb)
def register_exclusive(event, callback, *args, **kwargs):
cb = callback
if args or kwargs:
cb = lambda: callback(*args, **kwargs)
__callbacks[event] = [cb]
def unregister(event): def unregister(event):
if event in __callbacks: if event in __callbacks:
del __callbacks[event] del __callbacks[event]

View file

@ -10,6 +10,7 @@ MIDDLE_MOUSE = 2
RIGHT_MOUSE = 3 RIGHT_MOUSE = 3
WHEEL_UP = 4 WHEEL_UP = 4
WHEEL_DOWN = 5 WHEEL_DOWN = 5
UPDATE = -1
def button_name(button): def button_name(button):
@ -23,6 +24,8 @@ def button_name(button):
return "wheel-up" return "wheel-up"
if button == WHEEL_DOWN: if button == WHEEL_DOWN:
return "wheel-down" return "wheel-down"
if button == UPDATE:
return "update"
return "n/a" return "n/a"
@ -51,10 +54,13 @@ def register(obj, button=None, cmd=None, wait=False):
event_id = __event_id(obj.id if obj is not None else "", button) event_id = __event_id(obj.id if obj is not None else "", button)
logging.debug("registering callback {}".format(event_id)) logging.debug("registering callback {}".format(event_id))
core.event.unregister(event_id) # make sure there's always only one input event core.event.unregister(event_id) # make sure there's always only one input event
if callable(cmd): if callable(cmd):
core.event.register(event_id, cmd) core.event.register_exclusive(event_id, cmd)
elif obj and hasattr(obj, cmd) and callable(getattr(obj, cmd)):
core.event.register_exclusive(event_id, lambda event: getattr(obj, cmd)(event))
else: else:
core.event.register(event_id, lambda event: __execute(event, cmd, wait)) core.event.register_exclusive(event_id, lambda event: __execute(event, cmd, wait))
def trigger(event): def trigger(event):

View file

@ -17,6 +17,27 @@ except Exception as e:
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def import_user(module_short, config, theme):
usermod = os.path.expanduser("~/.config/bumblebee-status/modules/{}.py".format(module_short))
if os.path.exists(usermod):
if hasattr(importlib, "machinery"):
log.debug("importing {} from user via machinery".format(module_short))
mod = importlib.machinery.SourceFileLoader("modules.{}".format(module_short),
os.path.expanduser(usermod)).load_module()
return getattr(mod, "Module")(config, theme)
else:
log.debug("importing {} from user via importlib.util".format(module_short))
try:
spec = importlib.util.spec_from_file_location("modules.{}".format(module_short), usermod)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod.Module(config, theme)
except Exception as e:
spec = importlib.util.find_spec("modules.{}".format(module_short), usermod)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod.Module(config, theme)
raise ImportError("not found")
"""Loads a module by name """Loads a module by name
@ -33,20 +54,25 @@ def load(module_name, config=core.config.Config([]), theme=None):
error = None error = None
module_short, alias = (module_name.split(":") + [module_name])[0:2] module_short, alias = (module_name.split(":") + [module_name])[0:2]
config.set("__alias__", alias) config.set("__alias__", alias)
for namespace in ["core", "contrib"]:
try:
mod = importlib.import_module("modules.core.{}".format(module_short))
log.debug("importing {} from core".format(module_short))
return getattr(mod, "Module")(config, theme)
except ImportError as e:
try: try:
mod = importlib.import_module( log.warning("failed to import {} from core: {}".format(module_short, e))
"modules.{}.{}".format(namespace, module_short) mod = importlib.import_module("modules.contrib.{}".format(module_short))
) log.debug("importing {} from contrib".format(module_short))
log.debug(
"importing {} from {}.{}".format(module_short, namespace, module_short)
)
return getattr(mod, "Module")(config, theme) return getattr(mod, "Module")(config, theme)
except ImportError as e: except ImportError as e:
log.debug("failed to import {}: {}".format(module_name, e)) try:
error = e log.warning("failed to import {} from system: {}".format(module_short, e))
log.fatal("failed to import {}: {}".format(module_name, error)) return import_user(module_short, config, theme)
return Error(config=config, module=module_name, error=error) except ImportError as e:
log.fatal("import failed: {}".format(e))
log.fatal("failed to import {}".format(module_short))
return Error(config=config, module=module_name, error="unable to load module")
class Module(core.input.Object): class Module(core.input.Object):
@ -69,6 +95,8 @@ class Module(core.input.Object):
self.alias = self.__config.get("__alias__", None) self.alias = self.__config.get("__alias__", None)
self.id = self.alias if self.alias else self.name self.id = self.alias if self.alias else self.name
self.next_update = None self.next_update = None
self.minimized = False
self.minimized = self.parameter("start-minimized", False)
self.theme = theme self.theme = theme
@ -100,6 +128,8 @@ class Module(core.input.Object):
for prefix in [self.name, self.module_name, self.alias]: for prefix in [self.name, self.module_name, self.alias]:
value = self.__config.get("{}.{}".format(prefix, key), value) value = self.__config.get("{}.{}".format(prefix, key), value)
if self.minimized:
value = self.__config.get("{}.minimized.{}".format(prefix, key), value)
return value return value
"""Set a parameter for this module """Set a parameter for this module
@ -123,7 +153,7 @@ class Module(core.input.Object):
def update_wrapper(self): def update_wrapper(self):
if self.background == True: if self.background == True:
if self.__thread and self.__thread.isAlive(): if self.__thread and self.__thread.is_alive():
return # skip this update interval return # skip this update interval
self.__thread = threading.Thread(target=self.internal_update, args=(True,)) self.__thread = threading.Thread(target=self.internal_update, args=(True,))
self.__thread.start() self.__thread.start()
@ -170,9 +200,9 @@ class Module(core.input.Object):
:rtype: bumblebee_status.widget.Widget :rtype: bumblebee_status.widget.Widget
""" """
def add_widget(self, full_text="", name=None): def add_widget(self, full_text="", name=None, hidden=False):
widget_id = "{}::{}".format(self.name, len(self.widgets())) widget_id = "{}::{}".format(self.name, len(self.widgets()))
widget = core.widget.Widget(full_text=full_text, name=name, widget_id=widget_id) widget = core.widget.Widget(full_text=full_text, name=name, widget_id=widget_id, hidden=hidden)
self.widgets().append(widget) self.widgets().append(widget)
widget.module = self widget.module = self
return widget return widget

View file

@ -57,6 +57,9 @@ class block(object):
def set(self, key, value): def set(self, key, value):
self.__attributes[key] = value self.__attributes[key] = value
def get(self, key, default=None):
return self.__attributes.get(key, default)
def is_pango(self, attr): def is_pango(self, attr):
if isinstance(attr, dict) and "pango" in attr: if isinstance(attr, dict) and "pango" in attr:
return True return True
@ -91,9 +94,17 @@ class block(object):
assign(self.__attributes, result, "background", "bg") assign(self.__attributes, result, "background", "bg")
if "full_text" in self.__attributes: if "full_text" in self.__attributes:
prefix = self.__pad(self.pangoize(self.__attributes.get("prefix")))
suffix = self.__pad(self.pangoize(self.__attributes.get("suffix")))
self.set("_prefix", prefix)
self.set("_suffix", suffix)
self.set("_raw", self.get("full_text"))
result["full_text"] = self.pangoize(result["full_text"]) result["full_text"] = self.pangoize(result["full_text"])
result["full_text"] = self.__format(self.__attributes["full_text"]) result["full_text"] = self.__format(self.__attributes["full_text"])
if "min-width" in self.__attributes and "padding" in self.__attributes:
self.set("min-width", self.__format(self.get("min-width")))
for k in [ for k in [
"name", "name",
"instance", "instance",
@ -123,11 +134,8 @@ class block(object):
def __format(self, text): def __format(self, text):
if text is None: if text is None:
return None return None
prefix = self.__pad(self.pangoize(self.__attributes.get("prefix"))) prefix = self.get("_prefix")
suffix = self.__pad(self.pangoize(self.__attributes.get("suffix"))) suffix = self.get("_suffix")
self.set("_prefix", prefix)
self.set("_suffix", suffix)
self.set("_raw", text)
return "{}{}{}".format(prefix, text, suffix) return "{}{}{}".format(prefix, text, suffix)
@ -158,6 +166,12 @@ class i3(object):
def toggle_minimize(self, event): def toggle_minimize(self, event):
widget_id = event["instance"] widget_id = event["instance"]
for module in self.__modules:
if module.widget(widget_id=widget_id) and util.format.asbool(module.parameter("minimize", False)) == True:
# this module can customly minimize
module.minimized = not module.minimized
return
if widget_id in self.__content: if widget_id in self.__content:
self.__content[widget_id]["minimized"] = not self.__content[widget_id]["minimized"] self.__content[widget_id]["minimized"] = not self.__content[widget_id]["minimized"]
@ -208,14 +222,22 @@ class i3(object):
def blocks(self, module): def blocks(self, module):
blocks = [] blocks = []
if module.minimized:
blocks.extend(self.separator_block(module, module.widgets()[0]))
blocks.append(self.__content_block(module, module.widgets()[0]))
return blocks
for widget in module.widgets(): for widget in module.widgets():
if widget.module and self.__config.autohide(widget.module.name): if widget.module and self.__config.autohide(widget.module.name):
if not any( if not any(
state in widget.state() for state in ["warning", "critical"] state in widget.state() for state in ["warning", "critical", "no-autohide"]
): ):
continue continue
if module.hidden(): if module.hidden():
continue continue
if widget.hidden:
continue
if "critical" in widget.state() and self.__config.errorhide(widget.module.name):
continue
blocks.extend(self.separator_block(module, widget)) blocks.extend(self.separator_block(module, widget))
blocks.append(self.__content_block(module, widget)) blocks.append(self.__content_block(module, widget))
core.event.trigger("next-widget") core.event.trigger("next-widget")

View file

@ -7,6 +7,7 @@ import glob
import core.event import core.event
import util.algorithm import util.algorithm
import util.xresources
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -16,6 +17,7 @@ PATHS = [
os.path.join(THEME_BASE_DIR, "../../themes"), os.path.join(THEME_BASE_DIR, "../../themes"),
os.path.expanduser("~/.config/bumblebee-status/themes"), os.path.expanduser("~/.config/bumblebee-status/themes"),
os.path.expanduser("~/.local/share/bumblebee-status/themes"), # PIP os.path.expanduser("~/.local/share/bumblebee-status/themes"), # PIP
"/usr/share/bumblebee-status/themes",
] ]
@ -89,13 +91,21 @@ class Theme(object):
try: try:
if isinstance(name, dict): if isinstance(name, dict):
return name return name
result = {}
if name.lower() == "wal": if name.lower() == "wal":
wal = self.__load_json("~/.cache/wal/colors.json") wal = self.__load_json("~/.cache/wal/colors.json")
result = {}
for field in ["special", "colors"]: for field in ["special", "colors"]:
for key in wal.get(field, {}): for key in wal.get(field, {}):
result[key] = wal[field][key] result[key] = wal[field][key]
return result if name.lower() == "xresources":
for key in ("background", "foreground"):
result[key] = xresources.query(key)
for i in range(16):
key = color + str(i)
result[key] = xresources.query(key)
return result
except Exception as e: except Exception as e:
log.error("failed to load colors: {}", e) log.error("failed to load colors: {}", e)

View file

@ -10,12 +10,13 @@ log = logging.getLogger(__name__)
class Widget(util.store.Store, core.input.Object): class Widget(util.store.Store, core.input.Object):
def __init__(self, full_text="", name=None, widget_id=None): def __init__(self, full_text="", name=None, widget_id=None, hidden=False):
super(Widget, self).__init__() super(Widget, self).__init__()
self.__full_text = full_text self.__full_text = full_text
self.module = None self.module = None
self.name = name self.name = name
self.id = widget_id or self.id self.id = widget_id or self.id
self.hidden = hidden
@property @property
def module(self): def module(self):

View file

@ -14,6 +14,7 @@ import threading
import core.module import core.module
import core.widget import core.widget
import core.decorators import core.decorators
import core.input
import util.cli import util.cli
@ -56,6 +57,8 @@ class Module(core.module.Module):
def __init__(self, config, theme): def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.updates)) super().__init__(config, theme, core.widget.Widget(self.updates))
self.__thread = None self.__thread = None
core.input.register(self, button=core.input.RIGHT_MOUSE,
cmd=self.updates)
def updates(self, widget): def updates(self, widget):
if widget.get("error"): if widget.get("error"):
@ -65,7 +68,7 @@ class Module(core.module.Module):
) )
def update(self): def update(self):
if self.__thread and self.__thread.isAlive(): if self.__thread and self.__thread.is_alive():
return return
self.__thread = threading.Thread(target=get_apt_check_info, args=(self,)) self.__thread = threading.Thread(target=get_apt_check_info, args=(self,))

View file

@ -54,7 +54,7 @@ class Module(core.module.Module):
def activate_layout(layout_path): def activate_layout(layout_path):
log.debug("activating layout") log.debug("activating layout")
log.debug(layout_path) log.debug(layout_path)
execute(layout_path) execute(layout_path, ignore_errors=True)
def popup(self, widget): def popup(self, widget):
"""Create Popup that allows the user to control their displays in one """Create Popup that allows the user to control their displays in one
@ -64,7 +64,7 @@ class Module(core.module.Module):
menu = popup.menu() menu = popup.menu()
menu.add_menuitem( menu.add_menuitem(
"arandr", "arandr",
callback=partial(execute, self.manager) callback=partial(execute, self.manager, ignore_errors=True)
) )
menu.add_separator() menu.add_separator()
@ -105,11 +105,12 @@ class Module(core.module.Module):
if count_on == 1: if count_on == 1:
log.info("attempted to turn off last display") log.info("attempted to turn off last display")
return return
execute("{} --output {} --off".format(self.toggle_cmd, display)) execute("{} --output {} --off".format(self.toggle_cmd, display), ignore_errors=True)
else: else:
log.debug("toggling on {}".format(display)) log.debug("toggling on {}".format(display))
execute( execute(
"{} --output {} --auto".format(self.toggle_cmd, display) "{} --output {} --auto".format(self.toggle_cmd, display),
ignore_errors=True
) )
@staticmethod @staticmethod
@ -120,7 +121,7 @@ class Module(core.module.Module):
connected). connected).
""" """
displays = {} displays = {}
for line in execute("xrandr -q").split("\n"): for line in execute("xrandr -q", ignore_errors=True).split("\n"):
if "connected" not in line: if "connected" not in line:
continue continue
is_on = bool(re.search(r"\d+x\d+\+(\d+)\+\d+", line)) is_on = bool(re.search(r"\d+x\d+\+(\d+)\+\d+", line))
@ -136,16 +137,19 @@ class Module(core.module.Module):
def _get_layouts(): def _get_layouts():
"""Loads and parses the arandr screen layout scripts.""" """Loads and parses the arandr screen layout scripts."""
layouts = {} layouts = {}
for filename in os.listdir(__screenlayout_dir__): try:
if fnmatch.fnmatch(filename, '*.sh'): for filename in os.listdir(__screenlayout_dir__):
fullpath = os.path.join(__screenlayout_dir__, filename) if fnmatch.fnmatch(filename, '*.sh'):
with open(fullpath, "r") as file: fullpath = os.path.join(__screenlayout_dir__, filename)
for line in file: with open(fullpath, "r") as file:
s_line = line.strip() for line in file:
if "xrandr" not in s_line: s_line = line.strip()
continue if "xrandr" not in s_line:
displays_in_file = Module._parse_layout(line) continue
layouts[filename] = displays_in_file displays_in_file = Module._parse_layout(line)
layouts[filename] = displays_in_file
except Exception as e:
log.error(str(e))
return layouts return layouts
@staticmethod @staticmethod

View file

@ -40,7 +40,7 @@ class Module(core.module.Module):
) )
if code == 0: if code == 0:
self.__packages = len(result.split("\n")) self.__packages = len(result.strip().split("\n"))
elif code == 2: elif code == 2:
self.__packages = 0 self.__packages = 0
else: else:

View file

@ -0,0 +1 @@
arch-update.py

View file

@ -0,0 +1 @@
battery-upower.py

View file

@ -106,7 +106,7 @@ class Module(core.module.Module):
) )
logging.debug("bt: toggling bluetooth") logging.debug("bt: toggling bluetooth")
util.cli.execute(cmd) util.cli.execute(cmd, ignore_errors=True)
def state(self, widget): def state(self, widget):
"""Get current state.""" """Get current state."""

View file

@ -69,7 +69,7 @@ class Module(core.module.Module):
) )
logging.debug("bt: toggling bluetooth") logging.debug("bt: toggling bluetooth")
core.util.execute(cmd) util.cli.execute(cmd, ignore_errors=True)
def state(self, widget): def state(self, widget):
"""Get current state.""" """Get current state."""

View file

@ -5,8 +5,6 @@ some media control bindings.
Left click toggles pause, scroll up skips the current song, scroll Left click toggles pause, scroll up skips the current song, scroll
down returns to the previous song. down returns to the previous song.
Requires the following library:
* subprocess
Parameters: Parameters:
* deadbeef.format: Format string (defaults to '{artist} - {title}') * deadbeef.format: Format string (defaults to '{artist} - {title}')
Available values are: {artist}, {title}, {album}, {length}, Available values are: {artist}, {title}, {album}, {length},

View file

@ -0,0 +1,42 @@
# pylint: disable=C0111,R0903
"""Toggle dunst notifications using dunstctl.
When notifications are paused using this module dunst doesn't get killed and
you'll keep getting notifications on the background that will be displayed when
unpausing. This is specially useful if you're using dunst's scripting
(https://wiki.archlinux.org/index.php/Dunst#Scripting), which requires dunst to
be running. Scripts will be executed when dunst gets unpaused.
Requires:
* dunst v1.5.0+
contributed by `cristianmiranda <https://github.com/cristianmiranda>`_ - many thanks!
contributed by `joachimmathes <https://github.com/joachimmathes>`_ - many thanks!
"""
import core.module
import core.widget
import core.input
import util.cli
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(""))
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.toggle_state)
self.__states = {"unknown": ["unknown", "critical"],
"true": ["muted", "warning"],
"false": ["unmuted"]}
def toggle_state(self, event):
util.cli.execute("dunstctl set-paused toggle", ignore_errors=True)
def state(self, widget):
return self.__states[self.__is_dunst_paused()]
def __is_dunst_paused(self):
result = util.cli.execute("dunstctl is-paused",
return_exitcode=True,
ignore_errors=True)
return result[1].rstrip() if result[0] == 0 else "unknown"

View file

@ -0,0 +1,113 @@
"""Display information about the currently running emerge process.
Requires the following executable:
* emerge
Parameters:
* emerge_status.format: Format string (defaults to '{current}/{total} {action} {category}/{pkg}')
This code is based on emerge_status module from p3status [1] original created by AnwariasEu.
[1] https://github.com/ultrabug/py3status/blob/master/py3status/modules/emerge_status.py
"""
import re
import copy
import core.module
import core.widget
import core.decorators
import util.cli
import util.format
class Module(core.module.Module):
@core.decorators.every(seconds=10)
def __init__(self, config, theme):
super().__init__(config, theme, [])
self.__format = self.parameter(
"format", "{current}/{total} {action} {category}/{pkg}"
)
self.__ret_default = {
"action": "",
"category": "",
"current": 0,
"pkg": "",
"total": 0,
}
def update(self):
response = {}
ret = copy.deepcopy(self.__ret_default)
if self.__emerge_running():
ret = self.__get_progress()
widget = self.widget("status")
if not widget:
widget = self.add_widget(name="status")
if ret["total"] == 0:
widget.full_text("emrg calculating...")
else:
widget.full_text(
" ".join(
self.__format.format(
current=ret["current"],
total=ret["total"],
action=ret["action"],
category=ret["category"],
pkg=ret["pkg"],
).split()
)
)
else:
self.clear_widgets()
def __emerge_running(self):
"""
Check if emerge is running.
Returns true if at least one instance of emerge is running.
"""
try:
util.cli.execute("pgrep emerge")
return True
except Exception:
return False
def __get_progress(self):
"""
Get current progress of emerge.
Returns a dict containing current and total value.
"""
input_data = []
ret = {}
# traverse emerge.log from bottom up to get latest information
last_lines = util.cli.execute("tail -50 /var/log/emerge.log")
input_data = last_lines.split("\n")
input_data.reverse()
for line in input_data:
if "*** terminating." in line:
# copy content of ret_default, not only the references
ret = copy.deepcopy(self.__ret_default)
break
else:
status_re = re.compile(
r"\((?P<cu>[\d]+) of (?P<t>[\d]+)\) "
r"(?P<a>[a-zA-Z/]+( [a-zA-Z]+)?) "
r"\((?P<ca>[\w\-]+)/(?P<p>[\w.]+)"
)
res = status_re.search(line)
if res is not None:
ret["action"] = res.group("a").lower()
ret["category"] = res.group("ca")
ret["current"] = res.group("cu")
ret["pkg"] = res.group("p")
ret["total"] = res.group("t")
break
return ret
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Fetch hard drive temeperature data from a hddtemp daemon """Fetch hard drive temperature data from a hddtemp daemon
that runs on localhost and default port (7634) that runs on localhost and default port (7634)
contributed by `somospocos <https://github.com/somospocos>`_ - many thanks! contributed by `somospocos <https://github.com/somospocos>`_ - many thanks!

View file

@ -19,13 +19,13 @@ class Module(core.module.Module):
def __init__(self, config, theme): def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.current_layout)) super().__init__(config, theme, core.widget.Widget(self.current_layout))
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__next_keymap) core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.next_keymap)
self.__current_layout = self.__get_current_layout() self.__current_layout = self.__get_current_layout()
def current_layout(self, _): def current_layout(self, _):
return self.__current_layout return self.__current_layout
def __next_keymap(self, event): def next_keymap(self, event):
util.cli.execute("xkb-switch -n", ignore_errors=True) util.cli.execute("xkb-switch -n", ignore_errors=True)
def __get_current_layout(self): def __get_current_layout(self):

View file

@ -0,0 +1 @@
layout-xkbswitch.py

View file

@ -0,0 +1,128 @@
"""
A module to show the currently active network connection (ethernet or wifi) and connection strength if the connection is wireless.
Requires the Python netifaces package and iw installed on Linux.
A simpler take on nic and network_traffic. No extra config necessary!
"""
import util.cli
import util.format
import core.module
import core.widget
import core.input
import netifaces
import socket
class Module(core.module.Module):
@core.decorators.every(seconds=5)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.network))
self.__is_wireless = False
self.__is_connected = False
self.__interface = None
self.__message = None
self.__signal = -110
# Get network information to display to the user
def network(self, widgets):
# Determine whether there is an internet connection
self.__is_connected = self.__attempt_connection()
# Attempt to extract a valid network interface device
try:
self.__interface = netifaces.gateways()["default"][netifaces.AF_INET][1]
except Exception:
self.__interface = None
# Check to see if the interface (if connected to the internet) is wireless
if self.__is_connected and self.__interface:
self.__is_wireless = self.__interface_is_wireless(self.__interface)
# setup message to send to the user
if not self.__is_connected or not self.__interface:
self.__message = "No connection"
elif not self.__is_wireless:
# Assuming that if user is connected via non-wireless means that it will be ethernet
self.__signal = -30
self.__message = "Ethernet"
else:
# We have a wireless connection
iw_dat = util.cli.execute("iwgetid")
has_ssid = "ESSID" in iw_dat
signal = self.__compute_signal(self.__interface)
# If signal is None, that means that we can't compute the default interface's signal strength
self.__signal = (
util.format.asint(signal, minimum=-110, maximum=-30) if signal else None
)
ssid = (
iw_dat[iw_dat.index(":") + 1 :].replace('"', "").strip()
if has_ssid
else "Unknown"
)
self.__message = self.__generate_wireles_message(ssid, self.__signal)
return self.__message
# State determined by signal strength
def state(self, widget):
if self.__compute_strength(self.__signal) < 50:
return "critical"
if self.__compute_strength(self.__signal) < 75:
return "warning"
return None
# manually done for better granularity / ease of parsing strength data
def __generate_wireles_message(self, ssid, signal):
computed_strength = self.__compute_strength(signal)
strength_str = str(computed_strength) if computed_strength else "?"
return "{} {}%".format(ssid, strength_str)
def __compute_strength(self, signal):
return int(100 * ((signal + 100) / 70.0)) if signal else None
# get signal strength in decibels/milliwat
def __compute_signal(self, interface):
# Get connection strength
cmd = "iwconfig {}".format(interface)
config_dat = " ".join(util.cli.execute(cmd).split())
config_tokens = config_dat.replace("=", " ").split()
# handle weird output
try:
signal = config_tokens[config_tokens.index("level") + 1]
except Exception:
signal = None
return signal
def __attempt_connection(self):
can_connect = False
try:
socket.create_connection(("1.1.1.1", 53))
can_connect = True
except Exception:
can_connect = False
return can_connect
def __interface_is_wireless(self, interface):
is_wireless = False
try:
with open("/proc/net/wireless", "r") as f:
is_wireless = interface in f.read()
f.close()
except Exception:
is_wireless = False
return is_wireless

View file

@ -97,9 +97,6 @@ class BandwidthInfo(object):
"""Return default active network adapter""" """Return default active network adapter"""
gateway = netifaces.gateways()["default"] gateway = netifaces.gateways()["default"]
if not gateway:
raise "No default gateway found"
return gateway[netifaces.AF_INET][1] return gateway[netifaces.AF_INET][1]
@classmethod @classmethod

View file

@ -4,11 +4,15 @@
Parameters: Parameters:
* nvidiagpu.format: Format string (defaults to '{name}: {temp}°C %{usedmem}/{totalmem} MiB') * nvidiagpu.format: Format string (defaults to '{name}: {temp}°C %{usedmem}/{totalmem} MiB')
Available values are: {name} {temp} {mem_used} {mem_total} {fanspeed} {clock_gpu} {clock_mem} Available values are: {name} {temp} {mem_used} {mem_total} {fanspeed} {clock_gpu} {clock_mem} {gpu_usage_pct} {mem_usage_pct} {mem_io_pct}
Requires nvidia-smi Requires nvidia-smi
contributed by `RileyRedpath <https://github.com/RileyRedpath>`_ - many thanks! contributed by `RileyRedpath <https://github.com/RileyRedpath>`_ - many thanks!
Note: mem_io_pct is (from `man nvidia-smi`):
> Percent of time over the past sample period during which global (device)
> memory was being read or written.
""" """
import core.module import core.module
@ -41,6 +45,9 @@ class Module(core.module.Module):
clockMem = "" clockMem = ""
clockGpu = "" clockGpu = ""
fanspeed = "" fanspeed = ""
gpuUsagePct = ""
memIoPct = ""
memUsage = "not found"
for item in sp.split("\n"): for item in sp.split("\n"):
try: try:
key, val = item.split(":") key, val = item.split(":")
@ -61,10 +68,18 @@ class Module(core.module.Module):
name = val name = val
elif key == "Fan Speed": elif key == "Fan Speed":
fanspeed = val.split(" ")[0] fanspeed = val.split(" ")[0]
elif title == "Utilization":
if key == "Gpu":
gpuUsagePct = val.split(" ")[0]
elif key == "Memory":
memIoPct = val.split(" ")[0]
except: except:
title = item.strip() title = item.strip()
if totalMem and usedMem:
memUsage = int(int(usedMem) / int(totalMem) * 100)
str_format = self.parameter( str_format = self.parameter(
"format", "{name}: {temp}°C {mem_used}/{mem_total} MiB" "format", "{name}: {temp}°C {mem_used}/{mem_total} MiB"
) )
@ -76,6 +91,9 @@ class Module(core.module.Module):
clock_gpu=clockGpu, clock_gpu=clockGpu,
clock_mem=clockMem, clock_mem=clockMem,
fanspeed=fanspeed, fanspeed=fanspeed,
gpu_usage_pct=gpuUsagePct,
mem_io_pct=memIoPct,
mem_usage_pct=memUsage,
) )

View file

@ -85,8 +85,15 @@ class Module(core.module.Module):
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__show_popup) core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__show_popup)
def octoprint_status(self, widget): def octoprint_status(self, widget):
if self.__octoprint_state == "Offline" or self.__octoprint_state == "Unknown": if (
return self.__octoprint_state self.__octoprint_state.startswith("Offline")
or self.__octoprint_state == "Unknown"
):
return (
(self.__octoprint_state[:25] + "...")
if len(self.__octoprint_state) > 25
else self.__octoprint_state
)
return ( return (
self.__octoprint_state self.__octoprint_state
+ " | B: " + " | B: "

View file

@ -0,0 +1,30 @@
"""Displays currently active gpu by optimus-manager
Requires the following packages:
* optimus-manager
"""
import core.module
import core.widget
import util.cli
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.output))
self.__gpumode = ""
def output(self, _):
return "GPU: {}".format(self.__gpumode)
def update(self):
cmd = "optimus-manager --print-mode"
output = util.cli.execute(cmd).strip()
if "intel" in output:
self.__gpumode = "Intel"
elif "nvidia" in output:
self.__gpumode = "Nvidia"
elif "amd" in output:
self.__gpumode = "AMD"

View file

@ -0,0 +1,45 @@
# pylint: disable=C0111,R0903
"""Displays the current date and time in Persian(Jalali) Calendar.
Requires the following python packages:
* jdatetime
Parameters:
* datetime.format: strftime()-compatible formatting string. default: "%A %d %B" e.g., "جمعه ۱۳ اسفند"
* datetime.locale: locale to use. default: "fa_IR"
"""
from __future__ import absolute_import
import jdatetime
import locale
import core.module
import core.widget
import core.input
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.full_text))
l = ("fa_IR", "UTF-8")
lcl = self.parameter("locale", ".".join(l))
try:
locale.setlocale(locale.LC_ALL, lcl.split("."))
except Exception as e:
locale.setlocale(locale.LC_ALL, ("fa_IR", "UTF-8"))
def default_format(self):
return "%A %d %B"
def full_text(self, widget):
enc = locale.getpreferredencoding()
fmt = self.parameter("format", self.default_format())
retval = jdatetime.datetime.now().strftime(fmt)
if hasattr(retval, "decode"):
return retval.decode(enc)
return retval
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -5,57 +5,116 @@
Requires the following executable: Requires the following executable:
* playerctl * playerctl
contributed by `smitajit <https://github.com/smitajit>`_ - many thanks! Parameters:
* playerctl.format: Format string (defaults to '{{artist}} - {{title}} {{duration(position)}}/{{duration(mpris:length)}}').
The format string is passed to 'playerctl -f' as an argument. Read `the README <https://github.com/altdesktop/playerctl#printing-properties-and-metadata>`_ for more information.
* playerctl.layout: Comma-separated list to change order of widgets (defaults to song, previous, pause, next)
Widget names are: playerctl.song, playerctl.prev, playerctl.pause, playerctl.next
* playerctl.args: The arguments added to playerctl.
You can check 'playerctl --help' or `its README <https://github.com/altdesktop/playerctl#using-the-cli>`_. For example, it could be '-p vlc,%any'.
Parameters are inspired by the `spotify` module, many thanks to its developers!
contributed by `smitajit <https://github.com/smitajit>`_ - many thanks!
""" """
import core.module import core.module
import core.widget import core.widget
import core.input import core.input
import util.cli import util.cli
import util.format
import logging
class Module(core.module.Module): class Module(core.module.Module):
def __init__(self,config , theme): def __init__(self, config, theme):
widgets = [ super(Module, self).__init__(config, theme, [])
core.widget.Widget(name="playerctl.prev"),
core.widget.Widget(name="playerctl.main", full_text=self.description),
core.widget.Widget(name="playerctl.next"),
]
super(Module, self).__init__(config, theme , widgets)
core.input.register(widgets[0], button=core.input.LEFT_MOUSE, self.background = True
cmd="playerctl previous")
core.input.register(widgets[1], button=core.input.LEFT_MOUSE,
cmd="playerctl play-pause")
core.input.register(widgets[2], button=core.input.LEFT_MOUSE,
cmd="playerctl next")
self._status = None self.__layout = util.format.aslist(
self._tags = None self.parameter(
"layout", "playerctl.prev, playerctl.song, playerctl.pause, playerctl.next"
)
)
def description(self, widget): self.__cmd = "playerctl " + self.parameter("args", "") + " "
return self._tags if self._tags else "..." self.__format = self.parameter("format", "{{artist}} - {{title}} {{duration(position)}}/{{duration(mpris:length)}}")
widget_map = {}
for widget_name in self.__layout:
widget = self.add_widget(name=widget_name)
if widget_name == "playerctl.prev":
widget_map[widget] = {
"button": core.input.LEFT_MOUSE,
"cmd": self.__cmd + "previous",
}
elif widget_name == "playerctl.pause":
widget_map[widget] = {
"button": core.input.LEFT_MOUSE,
"cmd": self.__cmd + "play-pause",
}
elif widget_name == "playerctl.next":
widget_map[widget] = {
"button": core.input.LEFT_MOUSE,
"cmd": self.__cmd + "next",
}
elif widget_name == "playerctl.song":
widget_map[widget] = [
{
"button": core.input.LEFT_MOUSE,
"cmd": self.__cmd + "play-pause",
}, {
"button": core.input.WHEEL_UP,
"cmd": self.__cmd + "next",
}, {
"button": core.input.WHEEL_DOWN,
"cmd": self.__cmd + "previous",
}
]
else:
raise KeyError(
"The playerctl module does not have a {widget_name!r} widget".format(
widget_name=widget_name
)
)
for widget, callback_options in widget_map.items():
if isinstance(callback_options, dict):
core.input.register(widget, **callback_options)
def update(self): def update(self):
self._load_song()
def state(self, widget):
if widget.name == "playerctl.prev":
return "prev"
if widget.name == "playerctl.next":
return "next"
return self._status
def _load_song(self):
info = ""
try: try:
status = util.cli.execute("playerctl status").lower() playback_status = str(util.cli.execute(self.__cmd + "status 2>&1 || true", shell = True)).strip()
info = util.cli.execute("playerctl metadata xesam:title") if playback_status == "No players found":
except : playback_status = None
self._status = None except Exception as e:
self._tags = None logging.exception(e)
return playback_status = None
self._status = status.split("\n")[0].lower() for widget in self.widgets():
self._tags = info.split("\n")[0][:20] if playback_status:
if widget.name == "playerctl.pause":
if playback_status == "Playing":
widget.set("state", "playing")
elif playback_status == "Paused":
widget.set("state", "paused")
elif playback_status == "Stopped":
widget.set("state", "stopped")
else:
widget.set("state", "")
elif widget.name == "playerctl.next":
widget.set("state", "next")
elif widget.name == "playerctl.prev":
widget.set("state", "prev")
elif widget.name == "playerctl.song":
widget.full_text(self.__get_song())
else:
widget.set("state", "")
widget.full_text(" ")
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 def __get_song(self):
try:
return str(util.cli.execute(self.__cmd + "metadata -f '" + self.__format + "'")).strip()
except Exception as e:
logging.exception(e)
return " "

View file

@ -101,7 +101,7 @@ class Module(core.module.Module):
def state(self, widget): def state(self, widget):
if self.__active: if self.__active:
return "copying" return ["copying", "no-autohide"]
return "pending" return "pending"

View file

@ -16,13 +16,13 @@ class Module(core.module.Module):
self.__ip = "" self.__ip = ""
def public_ip(self, widget): def public_ip(self, widget):
return self.__ip return self.__ip or "n/a"
def update(self): def update(self):
try: try:
self.__ip = util.location.public_ip() self.__ip = util.location.public_ip()
except Exception: except Exception:
self.__ip = "n/a" self.__ip = None
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,48 @@
"""Rofication indicator
https://github.com/DaveDavenport/Rofication
simple module to show an icon + the number of notifications stored in rofication
module will have normal highlighting if there are zero notifications,
"warning" highlighting if there are nonzero notifications,
"critical" highlighting if there are any critical notifications
"""
import core.module
import core.widget
import core.decorators
import sys
import socket
class Module(core.module.Module):
@core.decorators.every(seconds=5)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.full_text))
self.__critical = False
self.__numnotifications = 0
def full_text(self, widgets):
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
client.connect("/tmp/rofi_notification_daemon")
# below code will fetch two numbers in a list, e.g. ['22', '1']
# first is total number of notifications, second is number of critical notifications
client.sendall(bytes("num", "utf-8"))
val = client.recv(512)
val = val.decode("utf-8")
l = val.split('\n',2)
self.__numnotifications = int(l[0])
self.__critical = bool(int(l[1]))
return self.__numnotifications
def state(self, widget):
# rofication doesn't really support the idea of seen vs unseen notifications
# marking a message as "seen" actually just sets its urgency to normal
# so, doing highlighting if any notifications are present
if self.__critical:
return ["critical"]
elif self.__numnotifications:
return ["warning"]
return []
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -31,14 +31,13 @@ class Module(core.module.Module):
orientation = curr_orient orientation = curr_orient
break break
widget = self.widget(display) widget = self.widget(name=display)
if not widget: if not widget:
widget = self.add_widget(full_text=display, name=display) widget = self.add_widget(full_text=display, name=display)
core.input.register( core.input.register(
widget, button=core.input.LEFT_MOUSE, cmd=self.__toggle widget, button=core.input.LEFT_MOUSE, cmd=self.__toggle
) )
widget.set("orientation", orientation) widget.set("orientation", orientation)
widgets.append(widget)
def state(self, widget): def state(self, widget):
return widget.get("orientation", "normal") return widget.get("orientation", "normal")

View file

@ -55,7 +55,7 @@ class Module(core.module.Module):
self._state = [] self._state = []
self._newspaper_filename = tempfile.mktemp(".html") self._newspaper_file = tempfile.NamedTemporaryFile(mode="w", suffix=".html")
self._last_refresh = 0 self._last_refresh = 0
self._last_update = 0 self._last_update = 0
@ -308,10 +308,11 @@ class Module(core.module.Module):
while newspaper_items: while newspaper_items:
content += self._create_news_section(newspaper_items) content += self._create_news_section(newspaper_items)
open(self._newspaper_filename, "w").write( self._newspaper_file.write(
HTML_TEMPLATE.replace("[[CONTENT]]", content) HTML_TEMPLATE.replace("[[CONTENT]]", content)
) )
webbrowser.open("file://" + self._newspaper_filename) self._newspaper_file.flush()
webbrowser.open("file://" + self._newspaper_file.name)
self._update_history("newspaper") self._update_history("newspaper")
self._save_history() self._save_history()

View file

@ -4,6 +4,7 @@
"""Displays sensor temperature """Displays sensor temperature
Parameters: Parameters:
* sensors.use_sensors: whether to use the sensors command
* sensors.path: path to temperature file (default /sys/class/thermal/thermal_zone0/temp). * sensors.path: path to temperature file (default /sys/class/thermal/thermal_zone0/temp).
* sensors.json: if set to 'true', interpret sensors.path as JSON 'path' in the output * sensors.json: if set to 'true', interpret sensors.path as JSON 'path' in the output
of 'sensors -j' (i.e. <key1>/<key2>/.../<value>), for example, path could of 'sensors -j' (i.e. <key1>/<key2>/.../<value>), for example, path could
@ -18,6 +19,7 @@ contributed by `mijoharas <https://github.com/mijoharas>`_ - many thanks!
""" """
import re import re
import os
import json import json
import logging import logging
@ -46,22 +48,25 @@ class Module(core.module.Module):
self._json = util.format.asbool(self.parameter("json", False)) self._json = util.format.asbool(self.parameter("json", False))
self._freq = util.format.asbool(self.parameter("show_freq", True)) self._freq = util.format.asbool(self.parameter("show_freq", True))
core.input.register(self, button=core.input.LEFT_MOUSE, cmd="xsensors") core.input.register(self, button=core.input.LEFT_MOUSE, cmd="xsensors")
self.determine_method() self.use_sensors = self.determine_method()
def determine_method(self): def determine_method(self):
if util.format.asbool(self.parameter("use_sensors")) == True:
return True
if util.format.asbool(self.parameter("use_sensors")) == False:
return False
if self.parameter("path") != None and self._json == False: if self.parameter("path") != None and self._json == False:
self.use_sensors = False # use thermal zone return False
else: # try to use output of sensors -u
# try to use output of sensors -u try:
try: _ = util.cli.execute("sensors -u")
output = util.cli.execute("sensors -u") log.debug("Sensors command available")
self.use_sensors = True return True
log.debug("Sensors command available") except FileNotFoundError as e:
except FileNotFoundError as e: log.info(
log.info( "Sensors command not available, using /sys/class/thermal/thermal_zone*/"
"Sensors command not available, using /sys/class/thermal/thermal_zone*/" )
) return False
self.use_sensors = False
def _get_temp_from_sensors(self): def _get_temp_from_sensors(self):
if self._json == True: if self._json == True:
@ -92,22 +97,31 @@ class Module(core.module.Module):
def get_temp(self): def get_temp(self):
if self.use_sensors: if self.use_sensors:
temperature = self._get_temp_from_sensors()
log.debug("Retrieve temperature from sensors -u") log.debug("Retrieve temperature from sensors -u")
else: return self._get_temp_from_sensors()
try: try:
temperature = open( path = None
self.parameter("path", "/sys/class/thermal/thermal_zone0/temp") # use path provided by the user
).read()[:2] if self.parameter("path") is not None:
log.debug("retrieved temperature from /sys/class/") path = self.parameter("path")
# TODO: Iterate through all thermal zones to determine the correct one and use its value # find the thermal zone that provides cpu temperature
# https://unix.stackexchange.com/questions/304845/discrepancy-between-number-of-cores-and-thermal-zones-in-sys-class-thermal else:
for zone in os.listdir("/sys/class/thermal"):
except IOError: if not zone.startswith("thermal_zone"):
temperature = "unknown" continue
log.info("Can not determine temperature, please install lm-sensors") if open(f"/sys/class/thermal/{zone}/type").read().strip() != "x86_pkg_temp":
continue
return temperature path = f"/sys/class/thermal/{zone}/temp"
# use zone 0 as fallback
if path is None:
log.info("Can not determine temperature path, using thermal_zone0")
path = "/sys/class/thermal/thermal_zone0/temp"
log.debug(f"retrieving temperature from {path}")
# the values are t°C * 1000, so divide by 1000
return str(int(open(path).read()) / 1000)
except IOError:
log.info("Can not determine temperature, please install lm-sensors")
return "unknown"
def get_mhz(self): def get_mhz(self):
mhz = None mhz = None

View file

@ -47,13 +47,13 @@ class Module(core.module.Module):
self.__output = "please wait..." self.__output = "please wait..."
self.__current_thread = threading.Thread() self.__current_thread = threading.Thread()
# LMB and RMB will update output regardless of timer if self.parameter("scrolling.makewide") is None:
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.update) self.set("scrolling.makewide", False)
core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self.update)
def set_output(self, value): def set_output(self, value):
self.__output = value self.__output = value
@core.decorators.scrollable
def get_output(self, _): def get_output(self, _):
return self.__output return self.__output

View file

@ -4,12 +4,12 @@
when clicking on it. when clicking on it.
For more than one shortcut, the commands and labels are strings separated by For more than one shortcut, the commands and labels are strings separated by
a demiliter (; semicolon by default). a delimiter (; semicolon by default).
For example in order to create two shortcuts labeled A and B with commands For example in order to create two shortcuts labeled A and B with commands
cmdA and cmdB you could do: cmdA and cmdB you could do:
./bumblebee-status -m shortcut -p shortcut.cmd='ls;ps' shortcut.label='A;B' ./bumblebee-status -m shortcut -p shortcut.cmd='firefox https://www.google.com;google-chrome https://google.com' shortcut.label='Google (Firefox);Google (Chrome)'
Parameters: Parameters:
* shortcut.cmds : List of commands to execute * shortcut.cmds : List of commands to execute

View file

@ -10,7 +10,7 @@ Requires the following executables:
* smartctl * smartctl
Parameters: Parameters:
* smartstatus.display: how to display (defaults to 'combined', other choices: 'seperate' or 'singles') * smartstatus.display: how to display (defaults to 'combined', other choices: 'combined_singles', 'seperate' or 'singles')
* smartstatus.drives: in the case of singles which drives to display, separated comma list value, multiple accepted (defaults to 'sda', example:'sda,sdc') * smartstatus.drives: in the case of singles which drives to display, separated comma list value, multiple accepted (defaults to 'sda', example:'sda,sdc')
* smartstatus.show_names: boolean in the form of "True" or "False" to show the name of the drives in the form of sda, sbd, combined or none at all. * smartstatus.show_names: boolean in the form of "True" or "False" to show the name of the drives in the form of sda, sbd, combined or none at all.
""" """
@ -38,7 +38,7 @@ class Module(core.module.Module):
self.create_widgets() self.create_widgets()
def create_widgets(self): def create_widgets(self):
if self.display == "combined": if self.display == "combined" or self.display == "combined_singles":
widget = self.add_widget() widget = self.add_widget()
widget.set("device", "combined") widget.set("device", "combined")
widget.set("assessment", self.combined()) widget.set("assessment", self.combined())
@ -81,6 +81,8 @@ class Module(core.module.Module):
def combined(self): def combined(self):
for device in self.devices: for device in self.devices:
if self.display == "combined_singles" and device not in self.drives:
continue
result = self.smart(device) result = self.smart(device)
if result == "Fail": if result == "Fail":
return "Fail" return "Fail"

View file

@ -0,0 +1,58 @@
"""Shows status and load percentage of logitech's unifying device
Requires the following executable:
* solaar (from community)
contributed by `cambid <https://github.com/cambid>`_ - many thanks!
"""
import logging
import core.module
import core.widget
import core.decorators
import util.cli
class Module(core.module.Module):
@core.decorators.every(seconds=30)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.utilization))
self.__battery = self.parameter("device", "")
self.background = True
self.__battery_status = ""
self.__error = False
if self.__battery != "":
self.__cmd = f"solaar show '{self.__battery}'"
else:
self.__cmd = "solaar show"
@property
def __format(self):
return self.parameter("format", "{}")
def utilization(self, widget):
return self.__format.format(self.__battery_status)
def update(self):
self.__error = False
code, result = util.cli.execute(
self.__cmd, ignore_errors=True, return_exitcode=True
)
if code == 0:
for line in result.split('\n'):
if line.count('Battery') > 0:
self.__battery_status = line.split(':')[1].strip()
else:
self.__error = True
logging.error(f"solaar exited with {code}: {result}")
def state(self, widget):
if self.__error:
return "warning"
return "okay"
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -9,7 +9,6 @@ an example.
Requires the following libraries: Requires the following libraries:
* requests * requests
* regex
Parameters: Parameters:
* spaceapi.url: String representation of the api endpoint * spaceapi.url: String representation of the api endpoint

View file

@ -8,6 +8,10 @@ Parameters:
Available values are: {album}, {title}, {artist}, {trackNumber} Available values are: {album}, {title}, {artist}, {trackNumber}
* spotify.layout: Comma-separated list to change order of widgets (defaults to song, previous, pause, next) * spotify.layout: Comma-separated list to change order of widgets (defaults to song, previous, pause, next)
Widget names are: spotify.song, spotify.prev, spotify.pause, spotify.next Widget names are: spotify.song, spotify.prev, spotify.pause, spotify.next
* spotify.concise_controls: When enabled, allows spotify to be controlled from just the spotify.song widget.
Concise controls are: Left Click: Toggle Pause; Wheel Up: Next; Wheel Down; Previous.
* spotify.bus_name: String (defaults to `spotify`)
Available values: spotify, spotifyd
contributed by `yvesh <https://github.com/yvesh>`_ - many thanks! contributed by `yvesh <https://github.com/yvesh>`_ - many thanks!
@ -25,32 +29,97 @@ import core.input
import core.decorators import core.decorators
import util.format import util.format
import logging
class Module(core.module.Module): class Module(core.module.Module):
def __init__(self, config, theme): def __init__(self, config, theme):
super().__init__(config, theme, []) super().__init__(config, theme, [])
self.background = True
self.__bus_name = self.parameter("bus_name", "spotify")
self.__layout = util.format.aslist( self.__layout = util.format.aslist(
self.parameter( self.parameter(
"layout", "spotify.song,spotify.prev,spotify.pause,spotify.next", "layout", "spotify.song,spotify.prev,spotify.pause,spotify.next",
) )
) )
self.__bus = dbus.SessionBus()
self.__song = "" self.__song = ""
self.__pause = "" self.__pause = ""
self.__format = self.parameter("format", "{artist} - {title}") self.__format = self.parameter("format", "{artist} - {title}")
self.__cmd = "dbus-send --session --type=method_call --dest=org.mpris.MediaPlayer2.spotify \ if self.__bus_name == "spotifyd":
self.__cmd = "dbus-send --session --type=method_call --dest=org.mpris.MediaPlayer2.spotifyd \
/org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player." /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player."
else:
self.__cmd = "dbus-send --session --type=method_call --dest=org.mpris.MediaPlayer2.spotify \
/org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player."
widget_map = {}
for widget_name in self.__layout:
widget = self.add_widget(name=widget_name)
if widget_name == "spotify.prev":
widget_map[widget] = {
"button": core.input.LEFT_MOUSE,
"cmd": self.__cmd + "Previous",
}
widget.set("state", "prev")
elif widget_name == "spotify.pause":
widget_map[widget] = {
"button": core.input.LEFT_MOUSE,
"cmd": self.__cmd + "PlayPause",
}
elif widget_name == "spotify.next":
widget_map[widget] = {
"button": core.input.LEFT_MOUSE,
"cmd": self.__cmd + "Next",
}
widget.set("state", "next")
elif widget_name == "spotify.song":
if util.format.asbool(self.parameter("concise_controls", "false")):
widget_map[widget] = [
{
"button": core.input.LEFT_MOUSE,
"cmd": self.__cmd + "PlayPause",
}, {
"button": core.input.WHEEL_UP,
"cmd": self.__cmd + "Next",
}, {
"button": core.input.WHEEL_DOWN,
"cmd": self.__cmd + "Previous",
}
]
else:
raise KeyError(
"The spotify module does not have a {widget_name!r} widget".format(
widget_name=widget_name
)
)
# is there any reason the inputs can't be directly registered above?
for widget, callback_options in widget_map.items():
if isinstance(callback_options, dict):
core.input.register(widget, **callback_options)
elif isinstance(callback_options, list): # used by concise_controls
for opts in callback_options:
core.input.register(widget, **opts)
def hidden(self): def hidden(self):
return self.string_song == "" return self.string_song == ""
def __get_song(self): def __get_song(self):
bus = dbus.SessionBus() bus = self.__bus
spotify = bus.get_object( if self.__bus_name == "spotifyd":
"org.mpris.MediaPlayer2.spotify", "/org/mpris/MediaPlayer2" spotify = bus.get_object(
) "org.mpris.MediaPlayer2.spotifyd", "/org/mpris/MediaPlayer2"
)
else:
spotify = bus.get_object(
"org.mpris.MediaPlayer2.spotify", "/org/mpris/MediaPlayer2"
)
spotify_iface = dbus.Interface(spotify, "org.freedesktop.DBus.Properties") spotify_iface = dbus.Interface(spotify, "org.freedesktop.DBus.Properties")
props = spotify_iface.Get("org.mpris.MediaPlayer2.Player", "Metadata") props = spotify_iface.Get("org.mpris.MediaPlayer2.Player", "Metadata")
self.__song = self.__format.format( self.__song = self.__format.format(
@ -62,29 +131,22 @@ class Module(core.module.Module):
def update(self): def update(self):
try: try:
self.clear_widgets()
self.__get_song() self.__get_song()
widget_map = {} if self.__bus_name == "spotifyd":
for widget_name in self.__layout: bus = self.__bus.get_object(
widget = self.add_widget(name=widget_name) "org.mpris.MediaPlayer2.spotifyd", "/org/mpris/MediaPlayer2"
if widget_name == "spotify.prev": )
widget_map[widget] = { else:
"button": core.input.LEFT_MOUSE, bus = self.__bus.get_object(
"cmd": self.__cmd + "Previous", "org.mpris.MediaPlayer2.spotify", "/org/mpris/MediaPlayer2"
} )
widget.set("state", "prev")
elif widget_name == "spotify.pause": for widget in self.widgets():
widget_map[widget] = { if widget.name == "spotify.pause":
"button": core.input.LEFT_MOUSE,
"cmd": self.__cmd + "PlayPause",
}
playback_status = str( playback_status = str(
dbus.Interface( dbus.Interface(
dbus.SessionBus().get_object( bus,
"org.mpris.MediaPlayer2.spotify",
"/org/mpris/MediaPlayer2",
),
"org.freedesktop.DBus.Properties", "org.freedesktop.DBus.Properties",
).Get("org.mpris.MediaPlayer2.Player", "PlaybackStatus") ).Get("org.mpris.MediaPlayer2.Player", "PlaybackStatus")
) )
@ -92,25 +154,11 @@ class Module(core.module.Module):
widget.set("state", "playing") widget.set("state", "playing")
else: else:
widget.set("state", "paused") widget.set("state", "paused")
elif widget_name == "spotify.next": elif widget.name == "spotify.song":
widget_map[widget] = {
"button": core.input.LEFT_MOUSE,
"cmd": self.__cmd + "Next",
}
widget.set("state", "next")
elif widget_name == "spotify.song":
widget.set("state", "song") widget.set("state", "song")
widget.full_text(self.__song) widget.full_text(self.__song)
else:
raise KeyError(
"The spotify module does not have a {widget_name!r} widget".format(
widget_name=widget_name
)
)
for widget, callback_options in widget_map.items():
core.input.register(widget, **callback_options)
except Exception: except Exception as e:
self.__song = "" self.__song = ""
@property @property

View file

@ -3,9 +3,6 @@
"""Display a stock quote from finance.yahoo.com """Display a stock quote from finance.yahoo.com
Requires the following python packages:
* requests
Parameters: Parameters:
* stock.symbols : Comma-separated list of symbols to fetch * stock.symbols : Comma-separated list of symbols to fetch
* stock.change : Should we fetch change in stock value (defaults to True) * stock.change : Should we fetch change in stock value (defaults to True)

View file

@ -8,8 +8,8 @@ Requires the following python packages:
* python-dateutil * python-dateutil
Parameters: Parameters:
* cpu.lat : Latitude of your location * sun.lat : Latitude of your location
* cpu.lon : Longitude of your location * sun.lon : Longitude of your location
(if none of those are set, location is determined automatically via location APIs) (if none of those are set, location is determined automatically via location APIs)
@ -39,7 +39,11 @@ class Module(core.module.Module):
self.__sun = None self.__sun = None
if not lat or not lon: if not lat or not lon:
lat, lon = util.location.coordinates() try:
lat, lon = util.location.coordinates()
except Exception:
pass
if lat and lon: if lat and lon:
self.__sun = Sun(float(lat), float(lon)) self.__sun = Sun(float(lat), float(lon))
@ -55,6 +59,10 @@ class Module(core.module.Module):
return "n/a" return "n/a"
def __calculate_times(self): def __calculate_times(self):
if not self.__sun:
self.__sunset = self.__sunrise = None
return
self.__isup = False self.__isup = False
order_matters = True order_matters = True

View file

@ -21,6 +21,9 @@ Parameters:
* system.suspend: specify a command for suspending (defaults to 'i3exit suspend') * system.suspend: specify a command for suspending (defaults to 'i3exit suspend')
* system.hibernate: specify a command for hibernating (defaults to 'i3exit hibernate') * system.hibernate: specify a command for hibernating (defaults to 'i3exit hibernate')
Requirements:
tkinter (python3-tk package on debian based systems either you can install it as python package)
contributed by `bbernhard <https://github.com/bbernhard>`_ - many thanks! contributed by `bbernhard <https://github.com/bbernhard>`_ - many thanks!
""" """

View file

@ -0,0 +1,89 @@
# pylint: disable=C0111,R0903
"""
Displays the unread emails count for one or more Thunderbird inboxes
Parameters:
* thunderbird.home: Absolute path of your .thunderbird directory (e.g.: /home/pi/.thunderbird)
* thunderbird.inboxes: Comma separated values for all MSF inboxes and their parent directory (account) (e.g.: imap.gmail.com/INBOX.msf,outlook.office365.com/Work.msf)
Tips:
* You can run the following command in order to list all your Thunderbird inboxes
find ~/.thunderbird -name '*.msf' | awk -F '/' '{print $(NF-1)"/"$(NF)}'
contributed by `cristianmiranda <https://github.com/cristianmiranda>`_ - many thanks!
"""
import core.module
import core.widget
import core.decorators
import core.input
import util.cli
class Module(core.module.Module):
@core.decorators.every(minutes=1)
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.thunderbird))
self.__total = 0
self.__label = ""
self.__inboxes = []
self.__home = self.parameter("home", "")
inboxes = self.parameter("inboxes", "")
if inboxes:
self.__inboxes = util.format.aslist(inboxes)
def thunderbird(self, _):
return str(self.__label)
def update(self):
try:
self.__total = 0
self.__label = ""
stream = self.__getThunderbirdStream()
unread = self.__getUnreadMessagesByInbox(stream)
counts = []
for inbox in self.__inboxes:
count = unread[inbox]
self.__total += int(count)
counts.append(count)
self.__label = "/".join(counts)
except Exception as err:
self.__label = err
def __getThunderbirdStream(self):
cmd = (
"find "
+ self.__home
+ " -name '*.msf' -exec grep -REo 'A2=[0-9]' {} + | grep"
)
for inbox in self.__inboxes:
cmd += " -e {}".format(inbox)
cmd += "| awk -F / '{print $(NF-1)\"/\"$(NF)}'"
return util.cli.execute(cmd, shell=True).strip().split("\n")
def __getUnreadMessagesByInbox(self, stream):
unread = {}
for line in stream:
entry = line.split(":A2=")
inbox = entry[0]
count = entry[1]
unread[inbox] = count
return unread
def state(self, widget):
if self.__total > 0:
return ["warning"]
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -21,9 +21,10 @@ class Module(core.module.Module):
super().__init__(config, theme, core.widget.Widget(self.output)) super().__init__(config, theme, core.widget.Widget(self.output))
self.__doc = os.path.expanduser(self.parameter("file", "~/Documents/todo.txt")) self.__doc = os.path.expanduser(self.parameter("file", "~/Documents/todo.txt"))
self.__editor = self.parameter("editor", "xdg-open")
self.__todos = self.count_items() self.__todos = self.count_items()
core.input.register( core.input.register(
self, button=core.input.LEFT_MOUSE, cmd="xdg-open {}".format(self.__doc) self, button=core.input.LEFT_MOUSE, cmd="{} {}".format(self.__editor, self.__doc)
) )
def output(self, widget): def output(self, widget):
@ -39,11 +40,12 @@ class Module(core.module.Module):
def count_items(self): def count_items(self):
try: try:
i = -1 i = 0
with open(self.__doc) as f: with open(self.__doc) as f:
for i, l in enumerate(f): for l in f.readlines():
pass if l.strip() != '':
return i + 1 i += 1
return i
except Exception: except Exception:
return 0 return 0

View file

@ -0,0 +1,57 @@
# pylint: disable=C0111,R0903
"""Displays the number of todo items from an org-mode file
Parameters:
* todo_org.file: File to read TODOs from (defaults to ~/org/todo.org)
* todo_org.remaining: False by default. When true, will output the number of remaining todos instead of the number completed (i.e. 1/4 means 1 of 4 todos remaining, rather than 1 of 4 todos completed)
Based on the todo module by `codingo <https://github.com/codingo>`
"""
import re
import os.path
import core.module
import core.widget
import core.input
from util.format import asbool
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.output))
self.__todo_regex = re.compile("^\\s*\\*+\\s*TODO")
self.__done_regex = re.compile("^\\s*\\*+\\s*DONE")
self.__doc = os.path.expanduser(
self.parameter("file", "~/org/todo.org")
)
self.__remaining = asbool(self.parameter("remaining", "False"))
self.__todo, self.__total = self.count_items()
core.input.register(
self,
button=core.input.LEFT_MOUSE,
cmd="emacs {}".format(self.__doc)
)
def output(self, widget):
if self.__remaining:
return "TODO: {}/{}".format(self.__todo, self.__total)
return "TODO: {}/{}".format(self.__total-self.__todo, self.__total)
def update(self):
self.__todo, self.__total = self.count_items()
def count_items(self):
todo, total = 0, 0
try:
with open(self.__doc, "r") as f:
for line in f:
if self.__todo_regex.match(line.upper()) is not None:
todo += 1
total += 1
elif self.__done_regex.match(line.upper()) is not None:
total += 1
return todo, total
except OSError:
return -1, -1
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -12,6 +12,7 @@ Parameters:
* cpu.warning : Warning threshold in % of CPU usage (defaults to 70%) * cpu.warning : Warning threshold in % of CPU usage (defaults to 70%)
* cpu.critical: Critical threshold in % of CPU usage (defaults to 80%) * cpu.critical: Critical threshold in % of CPU usage (defaults to 80%)
* cpu.format : Format string (defaults to '{:.01f}%') * cpu.format : Format string (defaults to '{:.01f}%')
* cpu.percpu : If set to true, show each individual cpu (defaults to false)
""" """
import psutil import psutil
@ -20,12 +21,19 @@ import core.module
import core.widget import core.widget
import core.input import core.input
import util.format
class Module(core.module.Module): class Module(core.module.Module):
def __init__(self, config, theme): def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.utilization)) super().__init__(config, theme, [])
self.widget().set("theme.minwidth", self._format.format(100.0 - 10e-20)) self._percpu = util.format.asbool(self.parameter("percpu", False))
self._utilization = psutil.cpu_percent(percpu=False)
for idx, cpu_perc in enumerate(self.cpu_utilization()):
widget = self.add_widget(name="cpu#{}".format(idx), full_text=self.utilization)
widget.set("utilization", cpu_perc)
widget.set("theme.minwidth", self._format.format(100.0 - 10e-20))
core.input.register( core.input.register(
self, button=core.input.LEFT_MOUSE, cmd="gnome-system-monitor" self, button=core.input.LEFT_MOUSE, cmd="gnome-system-monitor"
) )
@ -34,14 +42,19 @@ class Module(core.module.Module):
def _format(self): def _format(self):
return self.parameter("format", "{:.01f}%") return self.parameter("format", "{:.01f}%")
def utilization(self, _): def utilization(self, widget):
return self._format.format(self._utilization) return self._format.format(widget.get("utilization", 0.0))
def cpu_utilization(self):
tmp = psutil.cpu_percent(percpu=self._percpu)
return tmp if self._percpu else [tmp]
def update(self): def update(self):
self._utilization = psutil.cpu_percent(percpu=False) for idx, cpu_perc in enumerate(self.cpu_utilization()):
self.widgets()[idx].set("utilization", cpu_perc)
def state(self, _): def state(self, widget):
return self.threshold_state(self._utilization, 70, 80) return self.threshold_state(widget.get("utilization", 0.0), 70, 80)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -21,7 +21,6 @@ class Module(core.module.Module):
super().__init__(config, theme, core.widget.Widget(self.full_text)) super().__init__(config, theme, core.widget.Widget(self.full_text))
core.input.register(self, button=core.input.LEFT_MOUSE, cmd="calendar") core.input.register(self, button=core.input.LEFT_MOUSE, cmd="calendar")
self._fmt = self.parameter("format", self.default_format())
l = locale.getdefaultlocale() l = locale.getdefaultlocale()
if not l or l == (None, None): if not l or l == (None, None):
l = ("en_US", "UTF-8") l = ("en_US", "UTF-8")
@ -36,7 +35,8 @@ class Module(core.module.Module):
def full_text(self, widget): def full_text(self, widget):
enc = locale.getpreferredencoding() enc = locale.getpreferredencoding()
retval = datetime.datetime.now().strftime(self._fmt) fmt = self.parameter("format", self.default_format())
retval = datetime.datetime.now().strftime(fmt)
if hasattr(retval, "decode"): if hasattr(retval, "decode"):
return retval.decode(enc) return retval.decode(enc)
return retval return retval

View file

@ -8,6 +8,7 @@ Parameters:
* disk.path: Path to calculate disk usage from (defaults to /) * disk.path: Path to calculate disk usage from (defaults to /)
* disk.open: Which application / file manager to launch (default xdg-open) * disk.open: Which application / file manager to launch (default xdg-open)
* disk.format: Format string, tags {path}, {used}, {left}, {size} and {percent} (defaults to '{path} {used}/{size} ({percent:05.02f}%)') * disk.format: Format string, tags {path}, {used}, {left}, {size} and {percent} (defaults to '{path} {used}/{size} ({percent:05.02f}%)')
* disk.system: Unit system to use - SI (KB, MB, ...) or IEC (KiB, MiB, ...) (defaults to 'IEC')
""" """
import os import os
@ -25,6 +26,7 @@ class Module(core.module.Module):
self._path = self.parameter("path", "/") self._path = self.parameter("path", "/")
self._format = self.parameter("format", "{used}/{size} ({percent:05.02f}%)") self._format = self.parameter("format", "{used}/{size} ({percent:05.02f}%)")
self._system = self.parameter("system", "IEC")
self._used = 0 self._used = 0
self._left = 0 self._left = 0
@ -38,9 +40,9 @@ class Module(core.module.Module):
) )
def diskspace(self, widget): def diskspace(self, widget):
used_str = util.format.byte(self._used) used_str = util.format.byte(self._used, sys=self._system)
size_str = util.format.byte(self._size) size_str = util.format.byte(self._size, sys=self._system)
left_str = util.format.byte(self._left) left_str = util.format.byte(self._left, sys=self._system)
percent_str = self._percent percent_str = self._percent
return self._format.format( return self._format.format(

View file

@ -0,0 +1,56 @@
# pylint: disable=C0111,R0903
"""Shows when a key is pressed
Parameters:
* keys.keys: Comma-separated list of keys to monitor (defaults to "")
"""
import core.module
import core.widget
import core.decorators
import core.event
import util.format
from pynput.keyboard import Listener
NAMES = {
"Key.cmd": "cmd",
"Key.ctrl": "ctrl",
"Key.shift": "shift",
"Key.alt": "alt",
}
class Module(core.module.Module):
@core.decorators.never
def __init__(self, config, theme):
super().__init__(config, theme, [])
self._listener = Listener(on_press=self._key_press, on_release=self._key_release)
self._keys = util.format.aslist(self.parameter("keys", "Key.cmd,Key.ctrl,Key.alt,Key.shift"))
for k in self._keys:
self.add_widget(name=k, full_text=self._display_name(k), hidden=True)
self._listener.start()
def _display_name(self, key):
return NAMES.get(key, key)
def _key_press(self, key):
key = str(key)
if not key in self._keys: return
self.widget(key).hidden = False
core.event.trigger("update", [self.id], redraw_only=False)
def _key_release(self, key):
key = str(key)
if not key in self._keys: return
self.widget(key).hidden = True
core.event.trigger("update", [self.id], redraw_only=False)
def state(self, widget):
return widget.name
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1,39 @@
# pylint: disable=C0111,R0903
"""Displays the current keyboard layout
Parameters:
* layout.device: The device ID of the keyboard (as reported by `xinput -list`), defaults to the core device
"""
import re
import core.widget
import core.module
import util.cli
from bumblebee_status.discover import utility
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config=config, theme=theme, widgets=core.widget.Widget(self.get_layout))
self._cmd = utility("get-kbd-layout")
keyboard = self.parameter("device", None)
if keyboard:
self._cmd += " {}".format(keyboard)
def get_layout(self, widget):
result = util.cli.execute(self._cmd, ignore_errors=True)
m = re.search("([a-zA-Z]+_)?([a-zA-Z]+)(\(([\w-]+)\))?", result)
if m:
layout = m.group(2)
variant = m.group(3)
return layout if not variant else "{} {}".format(layout, variant)
return "n/a"
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -0,0 +1 @@
layout-xkb.py

View file

@ -27,6 +27,7 @@ class Module(core.module.Module):
self._cpus = multiprocessing.cpu_count() self._cpus = multiprocessing.cpu_count()
except NotImplementedError as e: except NotImplementedError as e:
self._cpus = 1 self._cpus = 1
core.input.register( core.input.register(
self, button=core.input.LEFT_MOUSE, cmd="gnome-system-monitor" self, button=core.input.LEFT_MOUSE, cmd="gnome-system-monitor"
) )

View file

@ -41,18 +41,8 @@ class Module(core.module.Module):
return self._format.format(**self._mem) return self._format.format(**self._mem)
def update(self): def update(self):
data = {} data = self.__parse_meminfo()
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
if "MemAvailable" in data: if "MemAvailable" in data:
used = data["MemTotal"] - data["MemAvailable"] used = data["MemTotal"] - data["MemAvailable"]
else: else:
@ -78,5 +68,28 @@ class Module(core.module.Module):
return "warning" return "warning"
return None return None
def __parse_meminfo(self):
data = {}
with open("/proc/meminfo", "r") as f:
# https://bugs.python.org/issue32933
for line in f.readlines():
tmp = re.split(r"[:\s]+", line)
value = self.__parse_value(tmp)
data[tmp[0]] = value
return data
def __parse_value(self, data):
value = int(data[1])
if data[2] == "kB":
value = value * 1024
if data[2] == "mB":
value = value * 1024 * 1024
if data[2] == "gB":
value = value * 1024 * 1024 * 1024
return value
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -7,12 +7,15 @@ Requires the following python module:
Requires the following executable: Requires the following executable:
* iw * iw
* (until and including 2.0.5: iwgetid)
Parameters: Parameters:
* nic.exclude: Comma-separated list of interface prefixes to exclude (defaults to 'lo,virbr,docker,vboxnet,veth,br') * nic.exclude: Comma-separated list of interface prefixes (supporting regular expressions) to exclude (defaults to 'lo,virbr,docker,vboxnet,veth,br,.*:avahi')
* nic.include: Comma-separated list of interfaces to include * nic.include: Comma-separated list of interfaces to include
* nic.states: Comma-separated list of states to show (prefix with '^' to invert - i.e. ^down -> show all devices that are not in state down) * nic.states: Comma-separated list of states to show (prefix with '^' to invert - i.e. ^down -> show all devices that are not in state down)
* nic.format: Format string (defaults to '{intf} {state} {ip} {ssid}') * nic.format: Format string (defaults to '{intf} {state} {ip} {ssid} {strength}')
* nic.strength_warning: Integer to set the threshold for warning state (defaults to 50)
* nic.strength_critical: Integer to set the threshold for critical state (defaults to 30)
""" """
import re import re
@ -27,17 +30,14 @@ import util.format
class Module(core.module.Module): class Module(core.module.Module):
@core.decorators.every(seconds=10) @core.decorators.every(seconds=5)
def __init__(self, config, theme): def __init__(self, config, theme):
widgets = [] widgets = []
super().__init__(config, theme, widgets) super().__init__(config, theme, widgets)
self._exclude = tuple( self._exclude = util.format.aslist(
filter( self.parameter("exclude", "lo,virbr,docker,vboxnet,veth,br,.*:avahi")
len,
self.parameter("exclude", "lo,virbr,docker,vboxnet,veth,br").split(","),
)
) )
self._include = self.parameter("include", "").split(",") self._include = util.format.aslist(self.parameter("include", ""))
self._states = {"include": [], "exclude": []} self._states = {"include": [], "exclude": []}
for state in tuple( for state in tuple(
@ -47,7 +47,15 @@ class Module(core.module.Module):
self._states["exclude"].append(state[1:]) self._states["exclude"].append(state[1:])
else: else:
self._states["include"].append(state) self._states["include"].append(state)
self._format = self.parameter("format", "{intf} {state} {ip} {ssid}") self._format = self.parameter("format", "{intf} {state} {ip} {ssid} {strength}")
self._strength_threshold_critical = self.parameter("strength_critical", 30)
self._strength_threshold_warning = self.parameter("strength_warning", 50)
# Limits for the accepted dBm values of wifi strength
self.__strength_dbm_lower_bound = -110
self.__strength_dbm_upper_bound = -30
self.iw = shutil.which("iw") self.iw = shutil.which("iw")
self._update_widgets(widgets) self._update_widgets(widgets)
@ -66,6 +74,14 @@ class Module(core.module.Module):
iftype = "wireless" if self._iswlan(intf) else "wired" iftype = "wireless" if self._iswlan(intf) else "wired"
iftype = "tunnel" if self._istunnel(intf) else iftype iftype = "tunnel" if self._istunnel(intf) else iftype
# "strength" is none if interface type is not wlan
strength = widget.get("strength")
if self._iswlan(intf) and strength:
if strength < self._strength_threshold_critical:
states.append("critical")
elif strength < self._strength_threshold_warning:
states.append("warning")
states.append("{}-{}".format(iftype, widget.get("state"))) states.append("{}-{}".format(iftype, widget.get("state")))
return states return states
@ -89,11 +105,18 @@ class Module(core.module.Module):
return [] return []
return retval return retval
def _excluded(self, intf):
for e in self._exclude:
if re.match(e, intf):
return True
return False
def _update_widgets(self, widgets): def _update_widgets(self, widgets):
self.clear_widgets() self.clear_widgets()
interfaces = [ interfaces = []
i for i in netifaces.interfaces() if not i.startswith(self._exclude) for i in netifaces.interfaces():
] if not self._excluded(i):
interfaces.append(i)
interfaces.extend([i for i in netifaces.interfaces() if i in self._include]) interfaces.extend([i for i in netifaces.interfaces() if i in self._include])
for intf in interfaces: for intf in interfaces:
@ -111,6 +134,9 @@ class Module(core.module.Module):
): ):
continue continue
strength_dbm = self.get_strength_dbm(intf)
strength_percent = self.convert_strength_dbm_percent(strength_dbm)
widget = self.widget(intf) widget = self.widget(intf)
if not widget: if not widget:
widget = self.add_widget(name=intf) widget = self.add_widget(name=intf)
@ -121,22 +147,44 @@ class Module(core.module.Module):
ip=", ".join(addr), ip=", ".join(addr),
intf=intf, intf=intf,
state=state, state=state,
strength=str(strength_percent) + "%" if strength_percent else "",
ssid=self.get_ssid(intf), ssid=self.get_ssid(intf),
).split() ).split()
) )
) )
widget.set("intf", intf) widget.set("intf", intf)
widget.set("state", state) widget.set("state", state)
widget.set("strength", strength_percent)
def get_ssid(self, intf): def get_ssid(self, intf):
if self._iswlan(intf) and not self._istunnel(intf) and self.iw: if not self._iswlan(intf) or self._istunnel(intf) or not self.iw:
ssid = util.cli.execute("{} dev {} link".format(self.iw, intf)) return ""
found_ssid = re.findall("SSID:\s(.+)", ssid)
if len(found_ssid) > 0: iw_info = util.cli.execute("{} dev {} info".format(self.iw, intf))
return found_ssid[0] for line in iw_info.split("\n"):
else: match = re.match(r"^\s+ssid\s(.+)$", line)
return "" if match:
return match.group(1)
return "" return ""
def get_strength_dbm(self, intf):
if not self._iswlan(intf) or self._istunnel(intf) or not self.iw:
return None
with open("/proc/net/wireless", "r") as file:
for line in file:
if intf in line:
# Remove trailing . by slicing it off ;)
strength_dbm = line.split()[3][:-1]
return util.format.asint(strength_dbm,
minimum=self.__strength_dbm_lower_bound,
maximum=self.__strength_dbm_upper_bound)
return None
def convert_strength_dbm_percent(self, signal):
return int(100 * ((signal + 100) / 70.0)) if signal else None
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -54,7 +54,7 @@ def get_redshift_value(module):
for line in res.split("\n"): for line in res.split("\n"):
line = line.lower() line = line.lower()
if "temperature" in line: if "temperature" in line:
widget.set("temp", line.split(" ")[2]) widget.set("temp", line.split(" ")[2].upper())
if "period" in line: if "period" in line:
state = line.split(" ")[1] state = line.split(" ")[1]
if "day" in state: if "day" in state:
@ -101,7 +101,7 @@ class Module(core.module.Module):
return val return val
def update(self): def update(self):
if self.__thread is not None and self.__thread.isAlive(): if self.__thread is not None and self.__thread.is_alive():
return return
self.__thread = threading.Thread(target=get_redshift_value, args=(self,)) self.__thread = threading.Thread(target=get_redshift_value, args=(self,))
self.__thread.start() self.__thread.start()

View file

@ -9,7 +9,7 @@ Parameters:
import core.module import core.module
import core.widget import core.widget
import core.decorators import core.decorators
import core.input
class Module(core.module.Module): class Module(core.module.Module):
@core.decorators.every(minutes=60) @core.decorators.every(minutes=60)
@ -20,5 +20,8 @@ class Module(core.module.Module):
def text(self, _): def text(self, _):
return self.__text return self.__text
def update_text(self, event):
self.__text = core.input.button_name(event["button"])
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -12,7 +12,6 @@ from .datetime import Module
class Module(Module): class Module(Module):
@core.decorators.every(seconds=59) # ensures one update per minute
def __init__(self, config, theme): def __init__(self, config, theme):
super().__init__(config, theme) super().__init__(config, theme)

View file

@ -71,22 +71,33 @@ def astemperature(val, unit="metric"):
return "{}°{}".format(int(val), __UNITS.get(unit.lower(), __UNITS["default"])) return "{}°{}".format(int(val), __UNITS.get(unit.lower(), __UNITS["default"]))
def byte(val, fmt="{:.2f}"): def byte(val, fmt="{:.2f}", sys="IEC"):
"""Returns a byte representation of the input value """Returns a byte representation of the input value
:param val: value to format, must be convertible into a float :param val: value to format, must be convertible into a float
:param fmt: optional output format string, defaults to {:.2f} :param fmt: optional output format string, defaults to {:.2f}
:param sys: optional unit system specifier - SI (kilo, Mega, Giga, ...) or
IEC (kibi, Mebi, Gibi, ...) - defaults to IEC
:return: byte representation (e.g. <X> KiB, GiB, etc.) of the input value :return: byte representation (e.g. <X> KiB, GiB, etc.) of the input value
:rtype: string :rtype: string
""" """
if sys == "IEC":
div = 1024.0
units = ["", "Ki", "Mi", "Gi", "Ti"]
final = "TiB"
elif sys == "SI":
div = 1000.0
units = ["", "K", "M", "G", "T"]
final = "TB"
val = float(val) val = float(val)
for unit in ["", "Ki", "Mi", "Gi"]: for unit in units:
if val < 1024.0: if val < div:
return "{}{}B".format(fmt, unit).format(val) return "{}{}B".format(fmt, unit).format(val)
val /= 1024.0 val /= div
return "{}GiB".format(fmt).format(val * 1024.0) return "{}{}".format(fmt).format(val * div, final)
__seconds_pattern = re.compile(r"(([\d\.?]+)h)?(([\d\.]+)m)?([\d\.]+)?s?") __seconds_pattern = re.compile(r"(([\d\.?]+)h)?(([\d\.]+)m)?([\d\.]+)?s?")

View file

@ -59,11 +59,11 @@ def __load():
__next = time.time() + 60 * 30 # error - try again every 30m __next = time.time() + 60 * 30 # error - try again every 30m
def __get(name, default=None): def __get(name):
global __data global __data
if not __data or __expired(): if not __data or __expired():
__load() __load()
return __data.get(name, default) return __data[name]
def reset(): def reset():

View file

@ -49,6 +49,7 @@ class menu(object):
return self._menu return self._menu
def __on_focus_out(self, event=None): def __on_focus_out(self, event=None):
self.running = False
self._root.destroy() self._root.destroy()
def __on_click(self, callback): def __on_click(self, callback):

View file

@ -0,0 +1,10 @@
import subprocess
import shutil
def query(key):
if shutil.which("xgetres"):
return subprocess.run(["xgetres", key],
capture_output=True).stdout.decode("utf-8").strip()
else:
raise Exception("xgetres must be installed for this theme")

View file

@ -1,34 +1,15 @@
General guidelines General guidelines
================== ==================
Writing unit tests Not much, right now. If you have an idea and some code, just
------------------ create a PR, I will gladly review and comment (and, likely, merge)
Some general hints: Just one minor note: ``bumblebee-status`` is mostly a one-person,
spare-time project, so please be patient when answering an issue,
question or PR takes a while.
- Tests should run with just Python Standard Library modules installed Also, the (small) community that has gathered around ``bumblebee-status``
(i.e. if there are additional requirements, the test should be skipped is extremely friendly and helpful, so don't hesitate to create issues
if those are missing) with questions, somebody will always come up with a useful answer.
- Tests should run even if there is no network connectivity (please mock
urllib calls, for example)
- Tests should be stable and not require modifications every time the
tested code's implementation changes slightly (been there / done that)
Right now, ``bumblebee-status`` is moving away from Python's :)
built-in ``unittest`` framework (tests located inside ``tests/``)
and towards ``pytest`` (tests located inside ``pytests/``).
First implication: To run the new tests, you need to have ``pytest``
installed, it is not part of the Python Standard Library. Most
distributions call the package ``python-pytest`` or ``python3-pytest``
or something similar (or you just use ``pip install --use pytest``)
Aside from that, you just write your tests using ``pytest`` as usual,
with one big caveat:
**If** you create a new directory inside ``pytests/``, you need to
also create a file called ``__init__.py`` inside that, otherwise,
modules won't load correctly.
For examples, just browse the existing code. A good, minimal sample
for unit testing ``bumblebee-status`` is ``pytests/core/test_event.py``.

View file

@ -8,4 +8,5 @@ Developer's Guide
general general
module module
theme theme
testing

View file

@ -11,10 +11,12 @@ Adding a new module to ``bumblebee-status`` is straight-forward:
``bumblebee-status`` (i.e. a module called ``bumblebee-status`` (i.e. a module called
``bumblebee_status/modules/contrib/test.py`` will be loaded using ``bumblebee_status/modules/contrib/test.py`` will be loaded using
``bumblebee-status -m test``) ``bumblebee-status -m test``)
- Alternatively, you can put your module in ``~/.config/bumblebee-status/modules/``
- The module name must follow the `Python Naming Conventions <https://www.python.org/dev/peps/pep-0008/#package-and-module-names>`_
- See below for how to actually write the module - See below for how to actually write the module
- Test (run ``bumblebee-status`` in the CLI) - Test (run ``bumblebee-status`` in the CLI)
- Make sure your changes dont break anything: ``./coverage.sh`` - Make sure your changes dont break anything: ``./coverage.sh``
- If you want to do me favour, run your module through - If you want to do me a favour, run your module through
``black -t py34`` before submitting ``black -t py34`` before submitting
Pull requests Pull requests
@ -22,7 +24,7 @@ Pull requests
The project **gladly** accepts PRs for bugfixes, new functionality, new The project **gladly** accepts PRs for bugfixes, new functionality, new
modules, etc. When you feel comfortable with what youve developed, modules, etc. When you feel comfortable with what youve developed,
please just open a PR, somebody will look at it eventually :) Thanks! please just open a PR. Somebody will look at it eventually :) Thanks!
Coding guidelines Coding guidelines
----------------- -----------------
@ -65,7 +67,7 @@ Of modules and widgets
There are two important concepts for module writers: - A module is There are two important concepts for module writers: - A module is
something that offers a single set of coherent functionality - A module something that offers a single set of coherent functionality - A module
has 1 to n “widgets”, which translates to individual blocks in the i3bar has 1 to n “widgets”, which translates to individual blocks in the i3bar.
Very often, this is a 1:1 relationship, and a single module has a single Very often, this is a 1:1 relationship, and a single module has a single
widget. If thats the case for you, you can stop reading now :) widget. If thats the case for you, you can stop reading now :)

View file

@ -0,0 +1,33 @@
Testing guidelines
==================
Writing unit tests
------------------
Some general hints:
- Tests should run with just Python Standard Library modules installed
(i.e. if there are additional requirements, the test should be skipped
if those are missing)
- Tests should run even if there is no network connectivity (please mock
urllib calls, for example)
- Tests should be stable and not require modifications every time the
tested code's implementation changes slightly (been there / done that)
Right now, ``bumblebee-status`` uses the ``pytest`` framework, and its
unit tests are located inside the ``tests/`` subdirectory.
First implication: To run the new tests, you need to have ``pytest``
installed, it is not part of the Python Standard Library. Most
distributions call the package ``python-pytest`` or ``python3-pytest``
or something similar (or you just use ``pip install --use pytest``)
Aside from that, you just write your tests using ``pytest`` as usual,
with one big caveat:
**If** you create a new directory inside ``tests/``, you need to
also create a file called ``__init__.py`` inside that, otherwise,
modules won't load correctly.
For examples, just browse the existing code. A good, minimal sample
for unit testing ``bumblebee-status`` is ``tests/core/test_event.py``.

View file

@ -1,6 +1,25 @@
Advanced usage Advanced usage
=========================== ===========================
Intervals
---------
Some modules define their own update intervals (e.g. most modules that query
an online service), such as to not cause a storm of "once every second" queries.
For such modules, the "global" interval defined via the ``interval`` parameter effectively defines the
highest possible "resolution". If you have a global interval of 10s, for example,
any other module can update at 10s, 20s, 30s, etc., but not every 25s. The status
bar will internally always align to the next future time slot.
The update interval can also be changed on a per-module basis, like
this (overriding the default module interval indicated above):
.. code-block:: bash
$ ./bumblebee-status -m cpu memory -p cpu.interval=5s memory.interval=1m
Events Events
------ ------
@ -87,6 +106,19 @@ attention, it will remain hidden. Note that this parameter is specified
*in addition* to ``-m`` (i.e. to autohide the CPU module, you would use *in addition* to ``-m`` (i.e. to autohide the CPU module, you would use
``bumblebee-status -m cpu memory traffic -a cpu``). ``bumblebee-status -m cpu memory traffic -a cpu``).
Scrolling widget text
-----------------------
Some widgets support scrolling for long text (e.g. most music player
widgets, rss, etc.). Those have some additional settings for customizing
the scrolling behaviour, in particular:
- ``scrolling.width``: Desired width of the scrolling panel
- ``scrolling.makewide``: If set to true, extends texts shorter than
``scrolling.width`` to that width
- ``scrolling.bounce``: If set to true, bounces the text when it reaches
the end, otherwise, it behaves like marquee (scroll-through) text
- ``scrolling.speed``: Defines the scroll speed, in characters per update
Additional widget theme settings Additional widget theme settings
-------------------------------- --------------------------------
@ -128,6 +160,7 @@ Configuration files have the following format:
[core] [core]
modules = <comma-separated list of modules to load> modules = <comma-separated list of modules to load>
autohide = <comma-separated list of modules to hide, unless in warning/error state>
theme = <theme to use by default> theme = <theme to use by default>
[module-parameters] [module-parameters]

View file

@ -20,15 +20,15 @@ feature requests, etc. :)
Thanks a lot! Thanks a lot!
+------------------------------------+-------------------------+ +------------------------------------+------------------------------+
| **Required i3wm version** | 4.12+ | | **Required i3wm version** | 4.12+ |
+------------------------------------+-------------------------+ +------------------------------------+------------------------------+
| **Supported Python versions** | 3.4, 3.5, 3.6, 3.7, 3.8 | | **Supported Python versions** | 3.4, 3.5, 3.6, 3.7, 3.8, 3.9 |
+------------------------------------+-------------------------+ +------------------------------------+------------------------------+
| **Supported FontAwesome versions** | 4 only | | **Supported FontAwesome versions** | 4 only |
+------------------------------------+-------------------------+ +------------------------------------+------------------------------+
| **Per-module requirements** | see :doc:`modules` | | **Per-module requirements** | see :doc:`modules` |
+------------------------------------+-------------------------+ +------------------------------------+------------------------------+
see :doc:`FAQ` for details on this see :doc:`FAQ` for details on this

View file

@ -19,6 +19,9 @@ Installation
# will install bumblebee-status into ~/.local/bin/bumblebee-status # will install bumblebee-status into ~/.local/bin/bumblebee-status
pip install --user bumblebee-status pip install --user bumblebee-status
There is also a SlackBuild available here: [slackbuilds:bumblebee-status](http://slackbuilds.org/repository/14.2/desktop/bumblebee-status/) - many thanks to [@Tonus1](https://github.com/Tonus1)!
Dependencies Dependencies
------------ ------------
@ -56,12 +59,15 @@ To change the update interval, use:
$ ./bumblebee-status -m <list of modules> -p interval=<interval in seconds> $ ./bumblebee-status -m <list of modules> -p interval=<interval in seconds>
The update interval can also be changed on a per-module basis, like The update interval is the global "refresh" interval of the modules (i.e. how often
this: the bar will be updated with new data). The default interval is one second. It is
possible to use suffixes such as "m" (for minutes), or "h" for hours (e.g.
``-p interval=5m`` to update once every 5 minutes.
.. code-block:: bash Note that some modules define their own intervals (e.g. most modules that query
an online service), such as to not cause a storm of "once every second" queries.
$ ./bumblebee-status -m cpu memory -p cpu.interval=5s memory.interval=1m For more details on that, please refer to :doc:`features`.
All modules can be given “aliases” using ``<module name>:<alias>``, by All modules can be given “aliases” using ``<module name>:<alias>``, by
which they can be parametrized, for example: which they can be parametrized, for example:

View file

@ -22,6 +22,7 @@ Parameters:
* cpu.warning : Warning threshold in % of CPU usage (defaults to 70%) * cpu.warning : Warning threshold in % of CPU usage (defaults to 70%)
* cpu.critical: Critical threshold in % of CPU usage (defaults to 80%) * cpu.critical: Critical threshold in % of CPU usage (defaults to 80%)
* cpu.format : Format string (defaults to '{:.01f}%') * cpu.format : Format string (defaults to '{:.01f}%')
* cpu.percpu : If set to true, show each individual cpu (defaults to false)
.. image:: ../screenshots/cpu.png .. image:: ../screenshots/cpu.png
@ -63,6 +64,7 @@ Parameters:
* disk.path: Path to calculate disk usage from (defaults to /) * disk.path: Path to calculate disk usage from (defaults to /)
* disk.open: Which application / file manager to launch (default xdg-open) * disk.open: Which application / file manager to launch (default xdg-open)
* disk.format: Format string, tags {path}, {used}, {left}, {size} and {percent} (defaults to '{path} {used}/{size} ({percent:05.02f}%)') * disk.format: Format string, tags {path}, {used}, {left}, {size} and {percent} (defaults to '{path} {used}/{size} ({percent:05.02f}%)')
* disk.system: Unit system to use - SI (KB, MB, ...) or IEC (KiB, MiB, ...) (defaults to 'IEC')
.. image:: ../screenshots/disk.png .. image:: ../screenshots/disk.png
@ -83,11 +85,33 @@ Requires:
.. image:: ../screenshots/git.png .. image:: ../screenshots/git.png
keys
~~~~
Shows when a key is pressed
Parameters:
* keys.keys: Comma-separated list of keys to monitor (defaults to "")
layout-xkb layout-xkb
~~~~~~~~~~ ~~~~~~~~~~
Displays the current keyboard layout using libX11 Displays the current keyboard layout using libX11
Requires the following library:
* libX11.so.6
and python module:
* xkbgroup
Parameters:
* layout-xkb.showname: Boolean that indicate whether the full name should be displayed. Defaults to false (only the symbol will be displayed)
* layout-xkb.show_variant: Boolean that indecates whether the variant name should be displayed. Defaults to true.
layout_xkb
~~~~~~~~~~
Displays the current keyboard layout using libX11
Requires the following library: Requires the following library:
* libX11.so.6 * libX11.so.6
and python module: and python module:
@ -141,9 +165,10 @@ Requires the following python module:
Requires the following executable: Requires the following executable:
* iw * iw
* (until and including 2.0.5: iwgetid)
Parameters: Parameters:
* nic.exclude: Comma-separated list of interface prefixes to exclude (defaults to 'lo,virbr,docker,vboxnet,veth,br') * nic.exclude: Comma-separated list of interface prefixes (supporting regular expressions) to exclude (defaults to 'lo,virbr,docker,vboxnet,veth,br,.*:avahi')
* nic.include: Comma-separated list of interfaces to include * nic.include: Comma-separated list of interfaces to include
* nic.states: Comma-separated list of states to show (prefix with '^' to invert - i.e. ^down -> show all devices that are not in state down) * nic.states: Comma-separated list of states to show (prefix with '^' to invert - i.e. ^down -> show all devices that are not in state down)
* nic.format: Format string (defaults to '{intf} {state} {ip} {ssid}') * nic.format: Format string (defaults to '{intf} {state} {ip} {ssid}')
@ -278,6 +303,7 @@ Parameters:
* vault.location: Location of the password store (defaults to ~/.password-store) * vault.location: Location of the password store (defaults to ~/.password-store)
* vault.offx: x-axis offset of popup menu (defaults to 0) * vault.offx: x-axis offset of popup menu (defaults to 0)
* vault.offy: y-axis offset of popup menu (defaults to 0) * vault.offy: y-axis offset of popup menu (defaults to 0)
* vault.text: Text to display on the widget (defaults to <click-for-password>)
Many thanks to `bbernhard <https://github.com/bbernhard>`_ for the idea! Many thanks to `bbernhard <https://github.com/bbernhard>`_ for the idea!
@ -294,6 +320,9 @@ Parameters:
and appending a file '~/.config/i3/config.<screen name>' for every screen. and appending a file '~/.config/i3/config.<screen name>' for every screen.
* xrandr.autoupdate: If set to 'false', does *not* invoke xrandr automatically. Instead, the * xrandr.autoupdate: If set to 'false', does *not* invoke xrandr automatically. Instead, the
module will only refresh when displays are enabled or disabled (defaults to true) module will only refresh when displays are enabled or disabled (defaults to true)
* xrandr.exclude: Comma-separated list of display name prefixes to exclude
* xrandr.autotoggle: Boolean flag to automatically enable new displays (defaults to false)
* xrandr.autotoggle_side: Which side to put autotoggled displays on ('right' or 'left', defaults to 'right')
Requires the following python module: Requires the following python module:
* (optional) i3 - if present, the need for updating the widget list is auto-detected * (optional) i3 - if present, the need for updating the widget list is auto-detected
@ -362,6 +391,16 @@ Requires the following executable:
contributed by `lucassouto <https://github.com/lucassouto>`_ - many thanks! contributed by `lucassouto <https://github.com/lucassouto>`_ - many thanks!
arch_update
~~~~~~~~~~~
Check updates to Arch Linux.
Requires the following executable:
* checkupdates (from pacman-contrib)
contributed by `lucassouto <https://github.com/lucassouto>`_ - many thanks!
battery battery
~~~~~~~ ~~~~~~~
@ -392,6 +431,18 @@ Parameters:
contributed by `martindoublem <https://github.com/martindoublem>`_ - many thanks! contributed by `martindoublem <https://github.com/martindoublem>`_ - many thanks!
battery_upower
~~~~~~~~~~~~~~
Displays battery status, remaining percentage and charging information.
Parameters:
* battery-upower.warning : Warning threshold in % of remaining charge (defaults to 20)
* battery-upower.critical : Critical threshold in % of remaining charge (defaults to 10)
* battery-upower.showremaining : If set to true (default) shows the remaining time until the batteries are completely discharged
contributed by `martindoublem <https://github.com/martindoublem>`_ - many thanks!
bluetooth bluetooth
~~~~~~~~~ ~~~~~~~~~
@ -579,8 +630,6 @@ some media control bindings.
Left click toggles pause, scroll up skips the current song, scroll Left click toggles pause, scroll up skips the current song, scroll
down returns to the previous song. down returns to the previous song.
Requires the following library:
* subprocess
Parameters: Parameters:
* deadbeef.format: Format string (defaults to '{artist} - {title}') * deadbeef.format: Format string (defaults to '{artist} - {title}')
Available values are: {artist}, {title}, {album}, {length}, Available values are: {artist}, {title}, {album}, {length},
@ -636,9 +685,6 @@ Displays DNF package update information (<security>/<bugfixes>/<enhancements>/<o
Requires the following executable: Requires the following executable:
* dnf * dnf
Parameters:
* dnf.interval: Time in minutes between two consecutive update checks (defaults to 30 minutes)
.. image:: ../screenshots/dnf.png .. image:: ../screenshots/dnf.png
docker_ps docker_ps
@ -660,6 +706,40 @@ contributed by `eknoes <https://github.com/eknoes>`_ - many thanks!
.. image:: ../screenshots/dunst.png .. image:: ../screenshots/dunst.png
dunstctl
~~~~~~~~
Toggle dunst notifications using dunstctl.
When notifications are paused using this module dunst doesn't get killed and
you'll keep getting notifications on the background that will be displayed when
unpausing. This is specially useful if you're using dunst's scripting
(https://wiki.archlinux.org/index.php/Dunst#Scripting), which requires dunst to
be running. Scripts will be executed when dunst gets unpaused.
Requires:
* dunst v1.5.0+
contributed by `cristianmiranda <https://github.com/cristianmiranda>`_ - many thanks!
contributed by `joachimmathes <https://github.com/joachimmathes>`_ - many thanks!
.. image:: ../screenshots/dunstctl.png
emerge_status
~~~~~~~~~~~~~
Display information about the currently running emerge process.
Requires the following executable:
* emerge
Parameters:
* emerge_status.format: Format string (defaults to '{current}/{total} {action} {category}/{pkg}')
This code is based on `emerge_status module from p3status <https://github.com/ultrabug/py3status/blob/master/py3status/modules/emerge_status.py>`_ original created by `AnwariasEu <https://github.com/AnwariasEu>`_.
.. image:: ../screenshots/emerge_status.png
getcrypto getcrypto
~~~~~~~~~ ~~~~~~~~~
@ -715,7 +795,7 @@ contributed by `TheEdgeOfRage <https://github.com/TheEdgeOfRage>`_ - many thanks
hddtemp hddtemp
~~~~~~~ ~~~~~~~
Fetch hard drive temeperature data from a hddtemp daemon Fetch hard drive temperature data from a hddtemp daemon
that runs on localhost and default port (7634) that runs on localhost and default port (7634)
contributed by `somospocos <https://github.com/somospocos>`_ - many thanks! contributed by `somospocos <https://github.com/somospocos>`_ - many thanks!
@ -788,6 +868,16 @@ Requires the following executable:
contributed by `somospocos <https://github.com/somospocos>`_ - many thanks! contributed by `somospocos <https://github.com/somospocos>`_ - many thanks!
layout_xkbswitch
~~~~~~~~~~~~~~~~
Displays and changes the current keyboard layout
Requires the following executable:
* xkb-switch
contributed by `somospocos <https://github.com/somospocos>`_ - many thanks!
libvirtvms libvirtvms
~~~~~~~~~~ ~~~~~~~~~~
@ -984,6 +1074,16 @@ Displays information about the current song in vlc, audacious, bmp, xmms2, spoti
Requires the following executable: Requires the following executable:
* playerctl * playerctl
Parameters:
* playerctl.format: Format string (defaults to '{{artist}} - {{title}} {{duration(position)}}/{{duration(mpris:length)}}').
The format string is passed to 'playerctl -f' as an argument. Read `the README <https://github.com/altdesktop/playerctl#printing-properties-and-metadata>`_ for more information.
* playerctl.layout: Comma-separated list to change order of widgets (defaults to song, previous, pause, next)
Widget names are: playerctl.song, playerctl.prev, playerctl.pause, playerctl.next
* playerctl.args: The arguments added to playerctl.
You can check 'playerctl --help' or `its readme <https://github.com/altdesktop/playerctl#using-the-cli>`_. For example, it could be '-p vlc,%any'.
Parameters are inspired by the `spotify` module, many thanks to its developers!
contributed by `smitajit <https://github.com/smitajit>`_ - many thanks! contributed by `smitajit <https://github.com/smitajit>`_ - many thanks!
.. image:: ../screenshots/playerctl.png .. image:: ../screenshots/playerctl.png
@ -1070,6 +1170,17 @@ publicip
Displays public IP address Displays public IP address
rofication
~~~~~~~~~~
Rofication indicator
https://github.com/DaveDavenport/Rofication
simple module to show an icon + the number of notifications stored in rofication
module will have normal highlighting if there are zero notifications,
"warning" highlighting if there are nonzero notifications,
"critical" highlighting if there are any critical notifications
rotation rotation
~~~~~~~~ ~~~~~~~~
@ -1099,6 +1210,9 @@ sensors
Displays sensor temperature Displays sensor temperature
Parameters: Parameters:
* sensors.use_sensors: whether to use the 'sensors' command.
If set to 'false', the sysfs-interface at '/sys/class/thermal' is used.
If not set, 'sensors' will be used if available.
* sensors.path: path to temperature file (default /sys/class/thermal/thermal_zone0/temp). * sensors.path: path to temperature file (default /sys/class/thermal/thermal_zone0/temp).
* sensors.json: if set to 'true', interpret sensors.path as JSON 'path' in the output * sensors.json: if set to 'true', interpret sensors.path as JSON 'path' in the output
of 'sensors -j' (i.e. <key1>/<key2>/.../<value>), for example, path could of 'sensors -j' (i.e. <key1>/<key2>/.../<value>), for example, path could
@ -1147,12 +1261,12 @@ Shows a widget per user-defined shortcut and allows to define the behaviour
when clicking on it. when clicking on it.
For more than one shortcut, the commands and labels are strings separated by For more than one shortcut, the commands and labels are strings separated by
a demiliter (; semicolon by default). a delimiter (; semicolon by default).
For example in order to create two shortcuts labeled A and B with commands For example in order to create two shortcuts labeled A and B with commands
cmdA and cmdB you could do: cmdA and cmdB you could do:
./bumblebee-status -m shortcut -p shortcut.cmd='ls;ps' shortcut.label='A;B' ./bumblebee-status -m shortcut -p shortcut.cmd='firefox https://www.google.com;google-chrome https://google.com' shortcut.label='Google (Firefox);Google (Chrome)'
Parameters: Parameters:
* shortcut.cmds : List of commands to execute * shortcut.cmds : List of commands to execute
@ -1174,7 +1288,7 @@ Requires the following executables:
* smartctl * smartctl
Parameters: Parameters:
* smartstatus.display: how to display (defaults to 'combined', other choices: 'seperate' or 'singles') * smartstatus.display: how to display (defaults to 'combined', other choices: 'combined_singles', 'seperate' or 'singles')
* smartstatus.drives: in the case of singles which drives to display, separated comma list value, multiple accepted (defaults to 'sda', example:'sda,sdc') * smartstatus.drives: in the case of singles which drives to display, separated comma list value, multiple accepted (defaults to 'sda', example:'sda,sdc')
* smartstatus.show_names: boolean in the form of "True" or "False" to show the name of the drives in the form of sda, sbd, combined or none at all. * smartstatus.show_names: boolean in the form of "True" or "False" to show the name of the drives in the form of sda, sbd, combined or none at all.
@ -1187,7 +1301,6 @@ an example.
Requires the following libraries: Requires the following libraries:
* requests * requests
* regex
Parameters: Parameters:
* spaceapi.url: String representation of the api endpoint * spaceapi.url: String representation of the api endpoint
@ -1218,6 +1331,10 @@ Parameters:
Available values are: {album}, {title}, {artist}, {trackNumber} Available values are: {album}, {title}, {artist}, {trackNumber}
* spotify.layout: Comma-separated list to change order of widgets (defaults to song, previous, pause, next) * spotify.layout: Comma-separated list to change order of widgets (defaults to song, previous, pause, next)
Widget names are: spotify.song, spotify.prev, spotify.pause, spotify.next Widget names are: spotify.song, spotify.prev, spotify.pause, spotify.next
* spotify.concise_controls: When enabled, allows spotify to be controlled from just the spotify.song widget.
Concise controls are: Left Click: Toggle Pause; Wheel Up: Next; Wheel Down; Previous.
* spotify.bus_name: String (defaults to `spotify`)
Available values: spotify, spotifyd
contributed by `yvesh <https://github.com/yvesh>`_ - many thanks! contributed by `yvesh <https://github.com/yvesh>`_ - many thanks!
@ -1232,9 +1349,6 @@ stock
Display a stock quote from finance.yahoo.com Display a stock quote from finance.yahoo.com
Requires the following python packages:
* requests
Parameters: Parameters:
* stock.symbols : Comma-separated list of symbols to fetch * stock.symbols : Comma-separated list of symbols to fetch
* stock.change : Should we fetch change in stock value (defaults to True) * stock.change : Should we fetch change in stock value (defaults to True)
@ -1255,8 +1369,8 @@ Requires the following python packages:
* python-dateutil * python-dateutil
Parameters: Parameters:
* cpu.lat : Latitude of your location * sun.lat : Latitude of your location
* cpu.lon : Longitude of your location * sun.lon : Longitude of your location
(if none of those are set, location is determined automatically via location APIs) (if none of those are set, location is determined automatically via location APIs)
@ -1284,6 +1398,9 @@ Parameters:
* system.lock: specify a command for locking the screen (defaults to 'i3exit lock') * system.lock: specify a command for locking the screen (defaults to 'i3exit lock')
* system.suspend: specify a command for suspending (defaults to 'i3exit suspend') * system.suspend: specify a command for suspending (defaults to 'i3exit suspend')
* system.hibernate: specify a command for hibernating (defaults to 'i3exit hibernate') * system.hibernate: specify a command for hibernating (defaults to 'i3exit hibernate')
Requirements:
tkinter (python3-tk package on debian based systems either you can install it as python package)
contributed by `bbernhard <https://github.com/bbernhard>`_ - many thanks! contributed by `bbernhard <https://github.com/bbernhard>`_ - many thanks!
@ -1303,6 +1420,24 @@ contributed by `chdorb <https://github.com/chdorb>`_ - many thanks!
.. image:: ../screenshots/taskwarrior.png .. image:: ../screenshots/taskwarrior.png
thunderbird
~~~~~~~~~~~
Displays the unread emails count for one or more Thunderbird inboxes
Parameters:
* thunderbird.home: Absolute path of your .thunderbird directory (e.g.: /home/pi/.thunderbird)
* thunderbird.inboxes: Comma separated values for all MSF inboxes and their parent directory (account) (e.g.: imap.gmail.com/INBOX.msf,outlook.office365.com/Work.msf)
Tips:
* You can run the following command in order to list all your Thunderbird inboxes
find ~/.thunderbird -name '*.msf' | awk -F '/' '{print $(NF-1)"/"$(NF)}'
contributed by `cristianmiranda <https://github.com/cristianmiranda>`_ - many thanks!
.. image:: ../screenshots/thunderbird.png
timetz timetz
~~~~~~ ~~~~~~
@ -1343,6 +1478,15 @@ contributed by `codingo <https://github.com/codingo>`_ - many thanks!
.. image:: ../screenshots/todo.png .. image:: ../screenshots/todo.png
todo_org
~~~~~~~~
Displays the number of todo items from an org-mode file
Parameters:
* todo_org.file: File to read TODOs from (defaults to ~/org/todo.org)
* todo_org.remaining: False by default. When true, will output the number of remaining todos instead of the number completed (i.e. 1/4 means 1 of 4 todos remaining, rather than 1 of 4 todos completed)
Based on the todo module by `codingo <https://github.com/codingo>`
traffic traffic
~~~~~~~ ~~~~~~~

1
docs/requirements.txt Normal file
View file

@ -0,0 +1 @@
docutils<0.18

View file

@ -0,0 +1 @@
power

View file

@ -1 +1,2 @@
dbus dbus-python
power

View file

@ -1 +1,2 @@
requests requests
Babel

View file

@ -1 +0,0 @@
dunst

View file

@ -1 +0,0 @@
hddtemp

View file

@ -0,0 +1 @@
libvirt-python

View file

@ -0,0 +1,2 @@
Pillow
simplejson

View file

@ -1,3 +1 @@
requests requests
json
time

View file

@ -0,0 +1 @@
speedtest-cli

View file

@ -1 +1 @@
dbus dbus-python

View file

@ -1 +1 @@
tkinter Pillow # placeholder for tk

View file

@ -1 +1 @@
yubico python-yubico

View file

@ -0,0 +1 @@
setuptools

BIN
screenshots/dunstctl.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
screenshots/thunderbird.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -23,6 +23,7 @@ classifiers =
Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Topic :: Software Development :: Libraries Topic :: Software Development :: Libraries
Topic :: Software Development :: Internationalization Topic :: Software Development :: Internationalization
Topic :: Utilities Topic :: Utilities
@ -30,8 +31,8 @@ keywords = bumblebee-status
[options] [options]
include_package_data = True include_package_data = True
allow-all-external = yes allow_all_external = yes
trusted-host = trusted_host =
gitlab.* gitlab.*
bitbucket.org bitbucket.org
github.com github.com

View file

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
"""Setup file for bumbleestatus bar to allow pip install of full package""" """Setup file for bumbleestatus bar to allow pip install of full package"""
# -*- coding: utf8 - *- # -*- coding: utf8 - *-
from setuptools import setup from setuptools import setup, find_packages
import versioneer import versioneer
with open("requirements/base.txt") as f: with open("requirements/base.txt") as f:
@ -20,11 +20,9 @@ EXTRAS_REQUIREMENTS_MAP = {
"cpu2": read_module("cpu2"), "cpu2": read_module("cpu2"),
"currency": read_module("currency"), "currency": read_module("currency"),
"docker_ps": read_module("docker_ps"), "docker_ps": read_module("docker_ps"),
"dunst": read_module("dunst"),
"getcrypto": read_module("getcrypto"), "getcrypto": read_module("getcrypto"),
"git": read_module("git"), "git": read_module("git"),
"github": read_module("github"), "github": read_module("github"),
"hddtemp": read_module("hddtemp"),
"layout-xkb": read_module("layout_xkb"), "layout-xkb": read_module("layout_xkb"),
"memory": read_module("memory"), "memory": read_module("memory"),
"network_traffic": read_module("network_traffic"), "network_traffic": read_module("network_traffic"),
@ -59,4 +57,5 @@ setup(
("share/bumblebee-status/themes/icons", glob.glob("themes/icons/*.json")), ("share/bumblebee-status/themes/icons", glob.glob("themes/icons/*.json")),
("share/bumblebee-status/utility", glob.glob("bin/*")), ("share/bumblebee-status/utility", glob.glob("bin/*")),
], ],
packages=find_packages(exclude=["tests", "tests.*"])
) )

View file

@ -55,7 +55,8 @@ def test_importerror(mocker):
module = core.module.load(module_name="test", config=config) module = core.module.load(module_name="test", config=config)
assert module.__class__.__name__ == "Error" assert module.__class__.__name__ == "Error"
assert module.widget().full_text() == "test: some-error" assert module.widget().full_text() == "test: some-error" or \
module.widget().full_text() == "test: unable to load module"
def test_loadvalid_module(): def test_loadvalid_module():

View file

@ -26,6 +26,7 @@ def module_a(mocker):
widget = mocker.MagicMock() widget = mocker.MagicMock()
widget.full_text.return_value = "test" widget.full_text.return_value = "test"
widget.id = "a" widget.id = "a"
widget.hidden = False
return SampleModule(config=core.config.Config([]), widgets=[widget, widget, widget]) return SampleModule(config=core.config.Config([]), widgets=[widget, widget, widget])
@pytest.fixture @pytest.fixture

View file

@ -1,5 +1,159 @@
import pytest import pytest
import util.cli
import core.config
import modules.contrib.amixer
@pytest.fixture
def module_mock(request):
def _module_mock(config = []):
return modules.contrib.amixer.Module(
config=core.config.Config(config),
theme=None
)
yield _module_mock
@pytest.fixture
def amixer_mock():
def _mock(device='Master', volume='10%', state='on'):
return """
Simple mixer control '{device}',0
Capabilities: pvolume pswitch pswitch-joined
Playback channels: Front Left - Front Right
Limits: Playback 0 - 65536
Mono:
Front Left: Playback 55705 [{volume}%] [{state}]
Front Right: Playback 55705 [{volume}%] [{state}]
""".format(
device=device,
volume=volume,
state=state
)
return _mock
def test_load_module(): def test_load_module():
__import__("modules.contrib.amixer") __import__("modules.contrib.amixer")
def test_initial_full_text(module_mock, amixer_mock, mocker):
module = module_mock()
assert module.widget().full_text() == 'n/a'
def test_input_registration(mocker):
input_register = mocker.patch('core.input.register')
module = modules.contrib.amixer.Module(
config=core.config.Config([]),
theme=None
)
input_register.assert_any_call(
module,
button=core.input.WHEEL_DOWN,
cmd=module.decrease_volume
)
input_register.assert_any_call(
module,
button=core.input.WHEEL_UP,
cmd=module.increase_volume
)
input_register.assert_any_call(
module,
button=core.input.LEFT_MOUSE,
cmd=module.toggle
)
def test_volume_update(module_mock, amixer_mock, mocker):
mocker.patch(
'util.cli.execute',
return_value=amixer_mock(volume='25%', state='on')
)
module = module_mock()
widget = module.widget()
module.update()
assert widget.full_text() == '25%'
assert module.state(widget) == ['unmuted']
def test_muted_update(module_mock, amixer_mock, mocker):
mocker.patch(
'util.cli.execute',
return_value=amixer_mock(volume='50%', state='off')
)
module = module_mock()
widget = module.widget()
module.update()
assert widget.full_text() == '50%'
assert module.state(widget) == ['warning', 'muted']
def test_exception_update(module_mock, mocker):
mocker.patch(
'util.cli.execute',
side_effect=Exception
)
module = module_mock()
widget = module.widget()
module.update()
assert widget.full_text() == 'n/a'
def test_unavailable_amixer(module_mock, mocker):
mocker.patch('util.cli.execute', return_value='Invalid')
module = module_mock()
widget = module.widget()
module.update()
assert widget.full_text() == '0%'
def test_toggle(module_mock, mocker):
command = mocker.patch('util.cli.execute')
module = module_mock()
module.toggle(False)
command.assert_called_once_with('amixer -q set Master,0 toggle')
def test_default_volume(module_mock, mocker):
module = module_mock()
command = mocker.patch('util.cli.execute')
module.increase_volume(False)
command.assert_called_once_with('amixer -q set Master,0 4%+')
command = mocker.patch('util.cli.execute')
module.decrease_volume(False)
command.assert_called_once_with('amixer -q set Master,0 4%-')
def test_custom_volume(module_mock, mocker):
module = module_mock(['-p', 'amixer.percent_change=25'])
command = mocker.patch('util.cli.execute')
module.increase_volume(False)
command.assert_called_once_with('amixer -q set Master,0 25%+')
command = mocker.patch('util.cli.execute')
module.decrease_volume(False)
command.assert_called_once_with('amixer -q set Master,0 25%-')
def test_custom_device(module_mock, mocker):
mocker.patch('util.cli.execute')
module = module_mock(['-p', 'amixer.device=CustomMaster'])
command = mocker.patch('util.cli.execute')
module.toggle(False)
command.assert_called_once_with('amixer -q set CustomMaster toggle')
command = mocker.patch('util.cli.execute')
module.increase_volume(False)
command.assert_called_once_with('amixer -q set CustomMaster 4%+')
command = mocker.patch('util.cli.execute')
module.decrease_volume(False)
command.assert_called_once_with('amixer -q set CustomMaster 4%-')

View file

@ -1,5 +1,76 @@
import pytest import pytest
import util.cli
import core.config
import modules.contrib.arch_update
@pytest.fixture
def module():
module = modules.contrib.arch_update.Module(
config=core.config.Config([]),
theme=None
)
yield module
def test_load_module(): def test_load_module():
__import__("modules.contrib.arch-update") __import__("modules.contrib.arch-update")
def test_load_symbolic_link_module():
__import__("modules.contrib.arch_update")
def test_with_one_package(module, mocker):
command = mocker.patch(
'util.cli.execute',
return_value=(0, 'bumblebee 1.0.0')
)
module.update()
command.assert_called_with(
'checkupdates',
ignore_errors=True,
return_exitcode=True
)
widget = module.widget()
assert widget.full_text() == 'Update Arch: 1'
assert module.state(widget) == None
assert module.hidden() == False
def test_with_two_packages(module, mocker):
command = mocker.patch(
'util.cli.execute',
return_value=(0, 'bumblebee 1.0.0\ni3wm 3.5.7')
)
module.update()
widget = module.widget()
assert widget.full_text() == 'Update Arch: 2'
assert module.state(widget) == 'warning'
assert module.hidden() == False
def test_with_no_packages(module, mocker):
mocker.patch('util.cli.execute', return_value=(2, ''))
module.update()
widget = module.widget()
assert widget.full_text() == 'Update Arch: 0'
assert module.state(widget) == None
assert module.hidden() == True
def test_with_unknown_code(module, mocker):
mocker.patch('util.cli.execute', return_value=(99, 'error'))
logger = mocker.patch('logging.error')
module.update()
logger.assert_called_with('checkupdates exited with {}: {}'.format(99, 'error'))
widget = module.widget()
assert widget.full_text() == 'Update Arch: 0'
assert module.state(widget) == 'warning'
assert module.hidden() == False

Some files were not shown because too many files have changed in this diff Show more