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
*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
env:
global:
- CC_TEST_REPORTER_ID=40cb00907f7a10e04868e856570bb997ab9c42fd3b63d980f2b2269433195fdf
python:
- "3.4"
- "3.5"
- "3.6"
- "3.7"
before_install:
- sudo apt-get -qq update
- "3.8"
- "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:
- sudo apt-get install python-dbus
- pip install -U coverage==4.3 pytest pytest-mock
- pip install codeclimate-test-reporter
- 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
- pip install -U coverage pytest pytest-mock freezegun
- pip install 'pygit2<1' 'libvirt-python<6.3' 'feedparser<6' || true
- pip install $(cat requirements/modules/*.txt | cut -d ' ' -f 1 | sort -u)
script:
- coverage run --source=. -m pytest tests -v
- CODECLIMATE_REPO_TOKEN=40cb00907f7a10e04868e856570bb997ab9c42fd3b63d980f2b2269433195fdf codeclimate-test-reporter
addons:
code_climate:
repo_token: 40cb00907f7a10e04868e856570bb997ab9c42fd3b63d980f2b2269433195fdf
after_script:
- coverage xml
- ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT

View file

@ -1,6 +1,6 @@
# 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)
![AUR version (release)](https://img.shields.io/aur/version/bumblebee-status)
![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)
[![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)
[![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.**
@ -28,16 +30,14 @@ Thanks a lot!
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)
---
**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`!
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/)
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/)
---
@ -76,10 +76,16 @@ makepkg -sicr
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
[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.
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
## Normal usage
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,
"wheel-up": 4,
"wheel-down": 5,
"update": -1,
}
@ -20,7 +21,7 @@ def main():
parser.add_argument(
"-b",
"--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",
default="left-mouse",
)

View file

@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/env python3
import os
import sys
@ -6,7 +6,6 @@ import json
import time
import signal
import socket
import select
import logging
import threading
@ -39,41 +38,40 @@ class CommandSocket(object):
self.__socket.close()
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:
poll = select.poll()
poll.register(sys.stdin.fileno(), select.POLLIN)
poll.register(cmdsocket, select.POLLIN)
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():
@ -100,9 +98,13 @@ def main():
core.input.register(None, core.input.WHEEL_DOWN, "i3-msg workspace next_on_output")
update_lock = threading.Lock()
input_thread = threading.Thread(target=handle_input, args=(output, update_lock, ))
input_thread.daemon = True
input_thread.start()
event_thread = threading.Thread(target=handle_events, args=(config, update_lock, ))
event_thread.daemon = True
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):
if update_lock.acquire(blocking=False) == True:

View file

@ -147,6 +147,13 @@ class Config(util.store.Store):
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"
)
parser.add_argument(
"-c",
"--config-file",
action="store",
default=None,
help="Specify a configuration file to use"
)
parser.add_argument(
"-m", "--modules", nargs="+", action="append", default=[], help=MODULE_HELP
)
@ -172,6 +179,13 @@ class Config(util.store.Store):
default=[],
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(
"-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)
for cfg in [
"~/.bumblebee-status.conf",
"~/.config/bumblebee-status.conf",
"~/.config/bumblebee-status/config",
]:
if self.__args.config_file:
cfg = self.__args.config_file
cfg = os.path.expanduser(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]
for param in parameters:
@ -302,14 +321,21 @@ class Config(util.store.Store):
def iconset(self):
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
:rtype: list of strings
:return: True if module should be hidden automatically, False otherwise
:rtype: bool
"""
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

View file

@ -8,6 +8,13 @@ def register(event, callback, *args, **kwargs):
__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):
if event in __callbacks:
del __callbacks[event]

View file

@ -10,6 +10,7 @@ MIDDLE_MOUSE = 2
RIGHT_MOUSE = 3
WHEEL_UP = 4
WHEEL_DOWN = 5
UPDATE = -1
def button_name(button):
@ -23,6 +24,8 @@ def button_name(button):
return "wheel-up"
if button == WHEEL_DOWN:
return "wheel-down"
if button == UPDATE:
return "update"
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)
logging.debug("registering callback {}".format(event_id))
core.event.unregister(event_id) # make sure there's always only one input event
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:
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):

View file

@ -17,6 +17,27 @@ except Exception as e:
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
@ -33,20 +54,25 @@ def load(module_name, config=core.config.Config([]), theme=None):
error = None
module_short, alias = (module_name.split(":") + [module_name])[0:2]
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:
mod = importlib.import_module(
"modules.{}.{}".format(namespace, module_short)
)
log.debug(
"importing {} from {}.{}".format(module_short, namespace, module_short)
)
log.warning("failed to import {} from core: {}".format(module_short, e))
mod = importlib.import_module("modules.contrib.{}".format(module_short))
log.debug("importing {} from contrib".format(module_short))
return getattr(mod, "Module")(config, theme)
except ImportError as e:
log.debug("failed to import {}: {}".format(module_name, e))
error = e
log.fatal("failed to import {}: {}".format(module_name, error))
return Error(config=config, module=module_name, error=error)
try:
log.warning("failed to import {} from system: {}".format(module_short, e))
return import_user(module_short, config, theme)
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):
@ -69,6 +95,8 @@ class Module(core.input.Object):
self.alias = self.__config.get("__alias__", None)
self.id = self.alias if self.alias else self.name
self.next_update = None
self.minimized = False
self.minimized = self.parameter("start-minimized", False)
self.theme = theme
@ -100,6 +128,8 @@ class Module(core.input.Object):
for prefix in [self.name, self.module_name, self.alias]:
value = self.__config.get("{}.{}".format(prefix, key), value)
if self.minimized:
value = self.__config.get("{}.minimized.{}".format(prefix, key), value)
return value
"""Set a parameter for this module
@ -123,7 +153,7 @@ class Module(core.input.Object):
def update_wrapper(self):
if self.background == True:
if self.__thread and self.__thread.isAlive():
if self.__thread and self.__thread.is_alive():
return # skip this update interval
self.__thread = threading.Thread(target=self.internal_update, args=(True,))
self.__thread.start()
@ -170,9 +200,9 @@ class Module(core.input.Object):
: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 = 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)
widget.module = self
return widget

View file

@ -57,6 +57,9 @@ class block(object):
def set(self, key, value):
self.__attributes[key] = value
def get(self, key, default=None):
return self.__attributes.get(key, default)
def is_pango(self, attr):
if isinstance(attr, dict) and "pango" in attr:
return True
@ -91,9 +94,17 @@ class block(object):
assign(self.__attributes, result, "background", "bg")
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.__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 [
"name",
"instance",
@ -123,11 +134,8 @@ class block(object):
def __format(self, text):
if text is None:
return None
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", text)
prefix = self.get("_prefix")
suffix = self.get("_suffix")
return "{}{}{}".format(prefix, text, suffix)
@ -158,6 +166,12 @@ class i3(object):
def toggle_minimize(self, event):
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:
self.__content[widget_id]["minimized"] = not self.__content[widget_id]["minimized"]
@ -208,14 +222,22 @@ class i3(object):
def blocks(self, module):
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():
if widget.module and self.__config.autohide(widget.module.name):
if not any(
state in widget.state() for state in ["warning", "critical"]
state in widget.state() for state in ["warning", "critical", "no-autohide"]
):
continue
if module.hidden():
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.append(self.__content_block(module, widget))
core.event.trigger("next-widget")

View file

@ -7,6 +7,7 @@ import glob
import core.event
import util.algorithm
import util.xresources
log = logging.getLogger(__name__)
@ -16,6 +17,7 @@ PATHS = [
os.path.join(THEME_BASE_DIR, "../../themes"),
os.path.expanduser("~/.config/bumblebee-status/themes"),
os.path.expanduser("~/.local/share/bumblebee-status/themes"), # PIP
"/usr/share/bumblebee-status/themes",
]
@ -89,13 +91,21 @@ class Theme(object):
try:
if isinstance(name, dict):
return name
result = {}
if name.lower() == "wal":
wal = self.__load_json("~/.cache/wal/colors.json")
result = {}
for field in ["special", "colors"]:
for key in wal.get(field, {}):
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:
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):
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__()
self.__full_text = full_text
self.module = None
self.name = name
self.id = widget_id or self.id
self.hidden = hidden
@property
def module(self):

View file

@ -14,6 +14,7 @@ import threading
import core.module
import core.widget
import core.decorators
import core.input
import util.cli
@ -56,6 +57,8 @@ class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.updates))
self.__thread = None
core.input.register(self, button=core.input.RIGHT_MOUSE,
cmd=self.updates)
def updates(self, widget):
if widget.get("error"):
@ -65,7 +68,7 @@ class Module(core.module.Module):
)
def update(self):
if self.__thread and self.__thread.isAlive():
if self.__thread and self.__thread.is_alive():
return
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):
log.debug("activating layout")
log.debug(layout_path)
execute(layout_path)
execute(layout_path, ignore_errors=True)
def popup(self, widget):
"""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.add_menuitem(
"arandr",
callback=partial(execute, self.manager)
callback=partial(execute, self.manager, ignore_errors=True)
)
menu.add_separator()
@ -105,11 +105,12 @@ class Module(core.module.Module):
if count_on == 1:
log.info("attempted to turn off last display")
return
execute("{} --output {} --off".format(self.toggle_cmd, display))
execute("{} --output {} --off".format(self.toggle_cmd, display), ignore_errors=True)
else:
log.debug("toggling on {}".format(display))
execute(
"{} --output {} --auto".format(self.toggle_cmd, display)
"{} --output {} --auto".format(self.toggle_cmd, display),
ignore_errors=True
)
@staticmethod
@ -120,7 +121,7 @@ class Module(core.module.Module):
connected).
"""
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:
continue
is_on = bool(re.search(r"\d+x\d+\+(\d+)\+\d+", line))
@ -136,16 +137,19 @@ class Module(core.module.Module):
def _get_layouts():
"""Loads and parses the arandr screen layout scripts."""
layouts = {}
for filename in os.listdir(__screenlayout_dir__):
if fnmatch.fnmatch(filename, '*.sh'):
fullpath = os.path.join(__screenlayout_dir__, filename)
with open(fullpath, "r") as file:
for line in file:
s_line = line.strip()
if "xrandr" not in s_line:
continue
displays_in_file = Module._parse_layout(line)
layouts[filename] = displays_in_file
try:
for filename in os.listdir(__screenlayout_dir__):
if fnmatch.fnmatch(filename, '*.sh'):
fullpath = os.path.join(__screenlayout_dir__, filename)
with open(fullpath, "r") as file:
for line in file:
s_line = line.strip()
if "xrandr" not in s_line:
continue
displays_in_file = Module._parse_layout(line)
layouts[filename] = displays_in_file
except Exception as e:
log.error(str(e))
return layouts
@staticmethod

View file

@ -40,7 +40,7 @@ class Module(core.module.Module):
)
if code == 0:
self.__packages = len(result.split("\n"))
self.__packages = len(result.strip().split("\n"))
elif code == 2:
self.__packages = 0
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")
util.cli.execute(cmd)
util.cli.execute(cmd, ignore_errors=True)
def state(self, widget):
"""Get current state."""

View file

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

View file

@ -5,8 +5,6 @@ some media control bindings.
Left click toggles pause, scroll up skips the current song, scroll
down returns to the previous song.
Requires the following library:
* subprocess
Parameters:
* deadbeef.format: Format string (defaults to '{artist} - {title}')
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 -*-
"""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)
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):
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()
def current_layout(self, _):
return self.__current_layout
def __next_keymap(self, event):
def next_keymap(self, event):
util.cli.execute("xkb-switch -n", ignore_errors=True)
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"""
gateway = netifaces.gateways()["default"]
if not gateway:
raise "No default gateway found"
return gateway[netifaces.AF_INET][1]
@classmethod

View file

@ -4,11 +4,15 @@
Parameters:
* 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
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
@ -41,6 +45,9 @@ class Module(core.module.Module):
clockMem = ""
clockGpu = ""
fanspeed = ""
gpuUsagePct = ""
memIoPct = ""
memUsage = "not found"
for item in sp.split("\n"):
try:
key, val = item.split(":")
@ -61,10 +68,18 @@ class Module(core.module.Module):
name = val
elif key == "Fan Speed":
fanspeed = val.split(" ")[0]
elif title == "Utilization":
if key == "Gpu":
gpuUsagePct = val.split(" ")[0]
elif key == "Memory":
memIoPct = val.split(" ")[0]
except:
title = item.strip()
if totalMem and usedMem:
memUsage = int(int(usedMem) / int(totalMem) * 100)
str_format = self.parameter(
"format", "{name}: {temp}°C {mem_used}/{mem_total} MiB"
)
@ -76,6 +91,9 @@ class Module(core.module.Module):
clock_gpu=clockGpu,
clock_mem=clockMem,
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)
def octoprint_status(self, widget):
if self.__octoprint_state == "Offline" or self.__octoprint_state == "Unknown":
return self.__octoprint_state
if (
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 (
self.__octoprint_state
+ " | 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:
* 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.widget
import core.input
import util.cli
import util.format
import logging
class Module(core.module.Module):
def __init__(self,config , theme):
widgets = [
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)
def __init__(self, config, theme):
super(Module, self).__init__(config, theme, [])
core.input.register(widgets[0], button=core.input.LEFT_MOUSE,
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.background = True
self._status = None
self._tags = None
self.__layout = util.format.aslist(
self.parameter(
"layout", "playerctl.prev, playerctl.song, playerctl.pause, playerctl.next"
)
)
def description(self, widget):
return self._tags if self._tags else "..."
self.__cmd = "playerctl " + self.parameter("args", "") + " "
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):
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:
status = util.cli.execute("playerctl status").lower()
info = util.cli.execute("playerctl metadata xesam:title")
except :
self._status = None
self._tags = None
return
self._status = status.split("\n")[0].lower()
self._tags = info.split("\n")[0][:20]
playback_status = str(util.cli.execute(self.__cmd + "status 2>&1 || true", shell = True)).strip()
if playback_status == "No players found":
playback_status = None
except Exception as e:
logging.exception(e)
playback_status = None
for widget in self.widgets():
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):
if self.__active:
return "copying"
return ["copying", "no-autohide"]
return "pending"

View file

@ -16,13 +16,13 @@ class Module(core.module.Module):
self.__ip = ""
def public_ip(self, widget):
return self.__ip
return self.__ip or "n/a"
def update(self):
try:
self.__ip = util.location.public_ip()
except Exception:
self.__ip = "n/a"
self.__ip = None
# 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
break
widget = self.widget(display)
widget = self.widget(name=display)
if not widget:
widget = self.add_widget(full_text=display, name=display)
core.input.register(
widget, button=core.input.LEFT_MOUSE, cmd=self.__toggle
)
widget.set("orientation", orientation)
widgets.append(widget)
def state(self, widget):
return widget.get("orientation", "normal")

View file

@ -55,7 +55,7 @@ class Module(core.module.Module):
self._state = []
self._newspaper_filename = tempfile.mktemp(".html")
self._newspaper_file = tempfile.NamedTemporaryFile(mode="w", suffix=".html")
self._last_refresh = 0
self._last_update = 0
@ -308,10 +308,11 @@ class Module(core.module.Module):
while newspaper_items:
content += self._create_news_section(newspaper_items)
open(self._newspaper_filename, "w").write(
self._newspaper_file.write(
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._save_history()

View file

@ -4,6 +4,7 @@
"""Displays sensor temperature
Parameters:
* sensors.use_sensors: whether to use the sensors command
* 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
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 os
import json
import logging
@ -46,22 +48,25 @@ class Module(core.module.Module):
self._json = util.format.asbool(self.parameter("json", False))
self._freq = util.format.asbool(self.parameter("show_freq", True))
core.input.register(self, button=core.input.LEFT_MOUSE, cmd="xsensors")
self.determine_method()
self.use_sensors = self.determine_method()
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:
self.use_sensors = False # use thermal zone
else:
# try to use output of sensors -u
try:
output = util.cli.execute("sensors -u")
self.use_sensors = True
log.debug("Sensors command available")
except FileNotFoundError as e:
log.info(
"Sensors command not available, using /sys/class/thermal/thermal_zone*/"
)
self.use_sensors = False
return False
# try to use output of sensors -u
try:
_ = util.cli.execute("sensors -u")
log.debug("Sensors command available")
return True
except FileNotFoundError as e:
log.info(
"Sensors command not available, using /sys/class/thermal/thermal_zone*/"
)
return False
def _get_temp_from_sensors(self):
if self._json == True:
@ -92,22 +97,31 @@ class Module(core.module.Module):
def get_temp(self):
if self.use_sensors:
temperature = self._get_temp_from_sensors()
log.debug("Retrieve temperature from sensors -u")
else:
try:
temperature = open(
self.parameter("path", "/sys/class/thermal/thermal_zone0/temp")
).read()[:2]
log.debug("retrieved temperature from /sys/class/")
# TODO: Iterate through all thermal zones to determine the correct one and use its value
# https://unix.stackexchange.com/questions/304845/discrepancy-between-number-of-cores-and-thermal-zones-in-sys-class-thermal
except IOError:
temperature = "unknown"
log.info("Can not determine temperature, please install lm-sensors")
return temperature
return self._get_temp_from_sensors()
try:
path = None
# use path provided by the user
if self.parameter("path") is not None:
path = self.parameter("path")
# find the thermal zone that provides cpu temperature
else:
for zone in os.listdir("/sys/class/thermal"):
if not zone.startswith("thermal_zone"):
continue
if open(f"/sys/class/thermal/{zone}/type").read().strip() != "x86_pkg_temp":
continue
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):
mhz = None

View file

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

View file

@ -4,12 +4,12 @@
when clicking on it.
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
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:
* shortcut.cmds : List of commands to execute

View file

@ -10,7 +10,7 @@ Requires the following executables:
* smartctl
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.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()
def create_widgets(self):
if self.display == "combined":
if self.display == "combined" or self.display == "combined_singles":
widget = self.add_widget()
widget.set("device", "combined")
widget.set("assessment", self.combined())
@ -81,6 +81,8 @@ class Module(core.module.Module):
def combined(self):
for device in self.devices:
if self.display == "combined_singles" and device not in self.drives:
continue
result = self.smart(device)
if result == "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:
* requests
* regex
Parameters:
* spaceapi.url: String representation of the api endpoint

View file

@ -8,6 +8,10 @@ Parameters:
Available values are: {album}, {title}, {artist}, {trackNumber}
* 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
* 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!
@ -25,32 +29,97 @@ import core.input
import core.decorators
import util.format
import logging
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, [])
self.background = True
self.__bus_name = self.parameter("bus_name", "spotify")
self.__layout = util.format.aslist(
self.parameter(
"layout", "spotify.song,spotify.prev,spotify.pause,spotify.next",
)
)
self.__bus = dbus.SessionBus()
self.__song = ""
self.__pause = ""
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."
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):
return self.string_song == ""
def __get_song(self):
bus = dbus.SessionBus()
spotify = bus.get_object(
"org.mpris.MediaPlayer2.spotify", "/org/mpris/MediaPlayer2"
)
bus = self.__bus
if self.__bus_name == "spotifyd":
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")
props = spotify_iface.Get("org.mpris.MediaPlayer2.Player", "Metadata")
self.__song = self.__format.format(
@ -62,29 +131,22 @@ class Module(core.module.Module):
def update(self):
try:
self.clear_widgets()
self.__get_song()
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",
}
if self.__bus_name == "spotifyd":
bus = self.__bus.get_object(
"org.mpris.MediaPlayer2.spotifyd", "/org/mpris/MediaPlayer2"
)
else:
bus = self.__bus.get_object(
"org.mpris.MediaPlayer2.spotify", "/org/mpris/MediaPlayer2"
)
for widget in self.widgets():
if widget.name == "spotify.pause":
playback_status = str(
dbus.Interface(
dbus.SessionBus().get_object(
"org.mpris.MediaPlayer2.spotify",
"/org/mpris/MediaPlayer2",
),
bus,
"org.freedesktop.DBus.Properties",
).Get("org.mpris.MediaPlayer2.Player", "PlaybackStatus")
)
@ -92,25 +154,11 @@ class Module(core.module.Module):
widget.set("state", "playing")
else:
widget.set("state", "paused")
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":
elif widget.name == "spotify.song":
widget.set("state", "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 = ""
@property

View file

@ -3,9 +3,6 @@
"""Display a stock quote from finance.yahoo.com
Requires the following python packages:
* requests
Parameters:
* stock.symbols : Comma-separated list of symbols to fetch
* 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
Parameters:
* cpu.lat : Latitude of your location
* cpu.lon : Longitude of your location
* sun.lat : Latitude of your location
* sun.lon : Longitude of your location
(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
if not lat or not lon:
lat, lon = util.location.coordinates()
try:
lat, lon = util.location.coordinates()
except Exception:
pass
if lat and lon:
self.__sun = Sun(float(lat), float(lon))
@ -55,6 +59,10 @@ class Module(core.module.Module):
return "n/a"
def __calculate_times(self):
if not self.__sun:
self.__sunset = self.__sunrise = None
return
self.__isup = False
order_matters = True

View file

@ -21,6 +21,9 @@ Parameters:
* system.suspend: specify a command for suspending (defaults to 'i3exit suspend')
* 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!
"""

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))
self.__doc = os.path.expanduser(self.parameter("file", "~/Documents/todo.txt"))
self.__editor = self.parameter("editor", "xdg-open")
self.__todos = self.count_items()
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):
@ -39,11 +40,12 @@ class Module(core.module.Module):
def count_items(self):
try:
i = -1
i = 0
with open(self.__doc) as f:
for i, l in enumerate(f):
pass
return i + 1
for l in f.readlines():
if l.strip() != '':
i += 1
return i
except Exception:
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.critical: Critical threshold in % of CPU usage (defaults to 80%)
* cpu.format : Format string (defaults to '{:.01f}%')
* cpu.percpu : If set to true, show each individual cpu (defaults to false)
"""
import psutil
@ -20,12 +21,19 @@ import core.module
import core.widget
import core.input
import util.format
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.utilization))
self.widget().set("theme.minwidth", self._format.format(100.0 - 10e-20))
self._utilization = psutil.cpu_percent(percpu=False)
super().__init__(config, theme, [])
self._percpu = util.format.asbool(self.parameter("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(
self, button=core.input.LEFT_MOUSE, cmd="gnome-system-monitor"
)
@ -34,14 +42,19 @@ class Module(core.module.Module):
def _format(self):
return self.parameter("format", "{:.01f}%")
def utilization(self, _):
return self._format.format(self._utilization)
def utilization(self, widget):
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):
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, _):
return self.threshold_state(self._utilization, 70, 80)
def state(self, widget):
return self.threshold_state(widget.get("utilization", 0.0), 70, 80)
# 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))
core.input.register(self, button=core.input.LEFT_MOUSE, cmd="calendar")
self._fmt = self.parameter("format", self.default_format())
l = locale.getdefaultlocale()
if not l or l == (None, None):
l = ("en_US", "UTF-8")
@ -36,7 +35,8 @@ class Module(core.module.Module):
def full_text(self, widget):
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"):
return retval.decode(enc)
return retval

View file

@ -8,6 +8,7 @@ Parameters:
* disk.path: Path to calculate disk usage from (defaults to /)
* 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.system: Unit system to use - SI (KB, MB, ...) or IEC (KiB, MiB, ...) (defaults to 'IEC')
"""
import os
@ -25,6 +26,7 @@ class Module(core.module.Module):
self._path = self.parameter("path", "/")
self._format = self.parameter("format", "{used}/{size} ({percent:05.02f}%)")
self._system = self.parameter("system", "IEC")
self._used = 0
self._left = 0
@ -38,9 +40,9 @@ class Module(core.module.Module):
)
def diskspace(self, widget):
used_str = util.format.byte(self._used)
size_str = util.format.byte(self._size)
left_str = util.format.byte(self._left)
used_str = util.format.byte(self._used, sys=self._system)
size_str = util.format.byte(self._size, sys=self._system)
left_str = util.format.byte(self._left, sys=self._system)
percent_str = self._percent
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()
except NotImplementedError as e:
self._cpus = 1
core.input.register(
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)
def update(self):
data = {}
with open("/proc/meminfo", "r") as f:
for line in f:
tmp = re.split(r"[:\s]+", line)
value = int(tmp[1])
if tmp[2] == "kB":
value = value * 1024
if tmp[2] == "mB":
value = value * 1024 * 1024
if tmp[2] == "gB":
value = value * 1024 * 1024 * 1024
data[tmp[0]] = value
data = self.__parse_meminfo()
if "MemAvailable" in data:
used = data["MemTotal"] - data["MemAvailable"]
else:
@ -78,5 +68,28 @@ class Module(core.module.Module):
return "warning"
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

View file

@ -7,12 +7,15 @@ Requires the following python module:
Requires the following executable:
* iw
* (until and including 2.0.5: iwgetid)
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.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
@ -27,17 +30,14 @@ import util.format
class Module(core.module.Module):
@core.decorators.every(seconds=10)
@core.decorators.every(seconds=5)
def __init__(self, config, theme):
widgets = []
super().__init__(config, theme, widgets)
self._exclude = tuple(
filter(
len,
self.parameter("exclude", "lo,virbr,docker,vboxnet,veth,br").split(","),
)
self._exclude = util.format.aslist(
self.parameter("exclude", "lo,virbr,docker,vboxnet,veth,br,.*:avahi")
)
self._include = self.parameter("include", "").split(",")
self._include = util.format.aslist(self.parameter("include", ""))
self._states = {"include": [], "exclude": []}
for state in tuple(
@ -47,7 +47,15 @@ class Module(core.module.Module):
self._states["exclude"].append(state[1:])
else:
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._update_widgets(widgets)
@ -66,6 +74,14 @@ class Module(core.module.Module):
iftype = "wireless" if self._iswlan(intf) else "wired"
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")))
return states
@ -89,11 +105,18 @@ class Module(core.module.Module):
return []
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):
self.clear_widgets()
interfaces = [
i for i in netifaces.interfaces() if not i.startswith(self._exclude)
]
interfaces = []
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])
for intf in interfaces:
@ -111,6 +134,9 @@ class Module(core.module.Module):
):
continue
strength_dbm = self.get_strength_dbm(intf)
strength_percent = self.convert_strength_dbm_percent(strength_dbm)
widget = self.widget(intf)
if not widget:
widget = self.add_widget(name=intf)
@ -121,22 +147,44 @@ class Module(core.module.Module):
ip=", ".join(addr),
intf=intf,
state=state,
strength=str(strength_percent) + "%" if strength_percent else "",
ssid=self.get_ssid(intf),
).split()
)
)
widget.set("intf", intf)
widget.set("state", state)
widget.set("strength", strength_percent)
def get_ssid(self, intf):
if self._iswlan(intf) and not self._istunnel(intf) and self.iw:
ssid = util.cli.execute("{} dev {} link".format(self.iw, intf))
found_ssid = re.findall("SSID:\s(.+)", ssid)
if len(found_ssid) > 0:
return found_ssid[0]
else:
return ""
if not self._iswlan(intf) or self._istunnel(intf) or not self.iw:
return ""
iw_info = util.cli.execute("{} dev {} info".format(self.iw, intf))
for line in iw_info.split("\n"):
match = re.match(r"^\s+ssid\s(.+)$", line)
if match:
return match.group(1)
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

View file

@ -54,7 +54,7 @@ def get_redshift_value(module):
for line in res.split("\n"):
line = line.lower()
if "temperature" in line:
widget.set("temp", line.split(" ")[2])
widget.set("temp", line.split(" ")[2].upper())
if "period" in line:
state = line.split(" ")[1]
if "day" in state:
@ -101,7 +101,7 @@ class Module(core.module.Module):
return val
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
self.__thread = threading.Thread(target=get_redshift_value, args=(self,))
self.__thread.start()

View file

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

View file

@ -12,7 +12,6 @@ from .datetime import Module
class Module(Module):
@core.decorators.every(seconds=59) # ensures one update per minute
def __init__(self, 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"]))
def byte(val, fmt="{:.2f}"):
def byte(val, fmt="{:.2f}", sys="IEC"):
"""Returns a byte representation of the input value
:param val: value to format, must be convertible into a float
: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
: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)
for unit in ["", "Ki", "Mi", "Gi"]:
if val < 1024.0:
for unit in units:
if val < div:
return "{}{}B".format(fmt, unit).format(val)
val /= 1024.0
return "{}GiB".format(fmt).format(val * 1024.0)
val /= div
return "{}{}".format(fmt).format(val * div, final)
__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
def __get(name, default=None):
def __get(name):
global __data
if not __data or __expired():
__load()
return __data.get(name, default)
return __data[name]
def reset():

View file

@ -49,6 +49,7 @@ class menu(object):
return self._menu
def __on_focus_out(self, event=None):
self.running = False
self._root.destroy()
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
==================
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
(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)
Also, the (small) community that has gathered around ``bumblebee-status``
is extremely friendly and helpful, so don't hesitate to create issues
with questions, somebody will always come up with a useful answer.
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
module
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/modules/contrib/test.py`` will be loaded using
``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
- Test (run ``bumblebee-status`` in the CLI)
- 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
Pull requests
@ -22,7 +24,7 @@ Pull requests
The project **gladly** accepts PRs for bugfixes, new functionality, new
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
-----------------
@ -65,7 +67,7 @@ Of modules and widgets
There are two important concepts for module writers: - A module is
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
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
===========================
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
------
@ -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
``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
--------------------------------
@ -128,6 +160,7 @@ Configuration files have the following format:
[core]
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>
[module-parameters]

View file

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

View file

@ -19,6 +19,9 @@ Installation
# will install bumblebee-status into ~/.local/bin/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
------------
@ -56,12 +59,15 @@ To change the update interval, use:
$ ./bumblebee-status -m <list of modules> -p interval=<interval in seconds>
The update interval can also be changed on a per-module basis, like
this:
The update interval is the global "refresh" interval of the modules (i.e. how often
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
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.critical: Critical threshold in % of CPU usage (defaults to 80%)
* cpu.format : Format string (defaults to '{:.01f}%')
* cpu.percpu : If set to true, show each individual cpu (defaults to false)
.. image:: ../screenshots/cpu.png
@ -63,6 +64,7 @@ Parameters:
* disk.path: Path to calculate disk usage from (defaults to /)
* 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.system: Unit system to use - SI (KB, MB, ...) or IEC (KiB, MiB, ...) (defaults to 'IEC')
.. image:: ../screenshots/disk.png
@ -83,11 +85,33 @@ Requires:
.. 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
~~~~~~~~~~
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:
* libX11.so.6
and python module:
@ -141,9 +165,10 @@ Requires the following python module:
Requires the following executable:
* iw
* (until and including 2.0.5: iwgetid)
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.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}')
@ -278,6 +303,7 @@ Parameters:
* vault.location: Location of the password store (defaults to ~/.password-store)
* vault.offx: x-axis offset of popup menu (defaults to 0)
* vault.offy: y-axis offset of popup menu (defaults to 0)
* vault.text: Text to display on the widget (defaults to <click-for-password>)
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.
* 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)
* 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:
* (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!
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
~~~~~~~
@ -392,6 +431,18 @@ Parameters:
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
~~~~~~~~~
@ -579,8 +630,6 @@ some media control bindings.
Left click toggles pause, scroll up skips the current song, scroll
down returns to the previous song.
Requires the following library:
* subprocess
Parameters:
* deadbeef.format: Format string (defaults to '{artist} - {title}')
Available values are: {artist}, {title}, {album}, {length},
@ -636,9 +685,6 @@ Displays DNF package update information (<security>/<bugfixes>/<enhancements>/<o
Requires the following executable:
* dnf
Parameters:
* dnf.interval: Time in minutes between two consecutive update checks (defaults to 30 minutes)
.. image:: ../screenshots/dnf.png
docker_ps
@ -660,6 +706,40 @@ contributed by `eknoes <https://github.com/eknoes>`_ - many thanks!
.. 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
~~~~~~~~~
@ -715,7 +795,7 @@ contributed by `TheEdgeOfRage <https://github.com/TheEdgeOfRage>`_ - many thanks
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)
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!
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
~~~~~~~~~~
@ -984,6 +1074,16 @@ Displays information about the current song in vlc, audacious, bmp, xmms2, spoti
Requires the following executable:
* 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!
.. image:: ../screenshots/playerctl.png
@ -1070,6 +1170,17 @@ publicip
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
~~~~~~~~
@ -1099,6 +1210,9 @@ sensors
Displays sensor temperature
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.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
@ -1147,12 +1261,12 @@ Shows a widget per user-defined shortcut and allows to define the behaviour
when clicking on it.
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
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:
* shortcut.cmds : List of commands to execute
@ -1174,7 +1288,7 @@ Requires the following executables:
* smartctl
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.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:
* requests
* regex
Parameters:
* spaceapi.url: String representation of the api endpoint
@ -1218,6 +1331,10 @@ Parameters:
Available values are: {album}, {title}, {artist}, {trackNumber}
* 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
* 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!
@ -1232,9 +1349,6 @@ stock
Display a stock quote from finance.yahoo.com
Requires the following python packages:
* requests
Parameters:
* stock.symbols : Comma-separated list of symbols to fetch
* stock.change : Should we fetch change in stock value (defaults to True)
@ -1255,8 +1369,8 @@ Requires the following python packages:
* python-dateutil
Parameters:
* cpu.lat : Latitude of your location
* cpu.lon : Longitude of your location
* sun.lat : Latitude of your location
* sun.lon : Longitude of your location
(if none of those are set, location is determined automatically via location APIs)
@ -1285,6 +1399,9 @@ Parameters:
* system.suspend: specify a command for suspending (defaults to 'i3exit suspend')
* 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!
taskwarrior
@ -1303,6 +1420,24 @@ contributed by `chdorb <https://github.com/chdorb>`_ - many thanks!
.. 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
~~~~~~
@ -1343,6 +1478,15 @@ contributed by `codingo <https://github.com/codingo>`_ - many thanks!
.. 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
~~~~~~~

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

View file

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

View file

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

View file

@ -1,5 +1,159 @@
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():
__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 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():
__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