Merge branch 'main' into pactl_revert
This commit is contained in:
commit
0bf91c2f15
129 changed files with 3295 additions and 432 deletions
70
.github/workflows/codeql-analysis.yml
vendored
Normal file
70
.github/workflows/codeql-analysis.yml
vendored
Normal 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
2
.gitignore
vendored
|
@ -1,3 +1,5 @@
|
|||
*.o
|
||||
|
||||
# Vim swap files
|
||||
*swp
|
||||
*~
|
||||
|
|
6
.readthedocs.yaml
Normal file
6
.readthedocs.yaml
Normal file
|
@ -0,0 +1,6 @@
|
|||
version: 2
|
||||
|
||||
python:
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
||||
|
42
.travis.yml
42
.travis.yml
|
@ -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
|
||||
|
|
18
README.md
18
README.md
|
@ -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
BIN
bin/get-kbd-layout
Executable file
Binary file not shown.
|
@ -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",
|
||||
)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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,))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
1
bumblebee_status/modules/contrib/arch_update.py
Symbolic link
1
bumblebee_status/modules/contrib/arch_update.py
Symbolic link
|
@ -0,0 +1 @@
|
|||
arch-update.py
|
1
bumblebee_status/modules/contrib/battery_upower.py
Symbolic link
1
bumblebee_status/modules/contrib/battery_upower.py
Symbolic link
|
@ -0,0 +1 @@
|
|||
battery-upower.py
|
|
@ -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."""
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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},
|
||||
|
|
42
bumblebee_status/modules/contrib/dunstctl.py
Normal file
42
bumblebee_status/modules/contrib/dunstctl.py
Normal 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"
|
113
bumblebee_status/modules/contrib/emerge_status.py
Normal file
113
bumblebee_status/modules/contrib/emerge_status.py
Normal 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
|
|
@ -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!
|
||||
|
|
|
@ -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):
|
||||
|
|
1
bumblebee_status/modules/contrib/layout_xkbswitch.py
Symbolic link
1
bumblebee_status/modules/contrib/layout_xkbswitch.py
Symbolic link
|
@ -0,0 +1 @@
|
|||
layout-xkbswitch.py
|
128
bumblebee_status/modules/contrib/network.py
Normal file
128
bumblebee_status/modules/contrib/network.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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: "
|
||||
|
|
30
bumblebee_status/modules/contrib/optman.py
Normal file
30
bumblebee_status/modules/contrib/optman.py
Normal 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"
|
45
bumblebee_status/modules/contrib/persian_date.py
Normal file
45
bumblebee_status/modules/contrib/persian_date.py
Normal 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
|
|
@ -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 " "
|
||||
|
|
|
@ -101,7 +101,7 @@ class Module(core.module.Module):
|
|||
|
||||
def state(self, widget):
|
||||
if self.__active:
|
||||
return "copying"
|
||||
return ["copying", "no-autohide"]
|
||||
return "pending"
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
48
bumblebee_status/modules/contrib/rofication.py
Normal file
48
bumblebee_status/modules/contrib/rofication.py
Normal 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
|
|
@ -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")
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
58
bumblebee_status/modules/contrib/solaar.py
Normal file
58
bumblebee_status/modules/contrib/solaar.py
Normal 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
|
|
@ -9,7 +9,6 @@ an example.
|
|||
|
||||
Requires the following libraries:
|
||||
* requests
|
||||
* regex
|
||||
|
||||
Parameters:
|
||||
* spaceapi.url: String representation of the api endpoint
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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!
|
||||
"""
|
||||
|
||||
|
|
89
bumblebee_status/modules/contrib/thunderbird.py
Normal file
89
bumblebee_status/modules/contrib/thunderbird.py
Normal 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
|
|
@ -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
|
||||
|
||||
|
|
57
bumblebee_status/modules/contrib/todo_org.py
Normal file
57
bumblebee_status/modules/contrib/todo_org.py
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
56
bumblebee_status/modules/core/keys.py
Normal file
56
bumblebee_status/modules/core/keys.py
Normal 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
|
39
bumblebee_status/modules/core/layout.py
Normal file
39
bumblebee_status/modules/core/layout.py
Normal 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
|
1
bumblebee_status/modules/core/layout_xkb.py
Symbolic link
1
bumblebee_status/modules/core/layout_xkb.py
Symbolic link
|
@ -0,0 +1 @@
|
|||
layout-xkb.py
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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?")
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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):
|
||||
|
|
10
bumblebee_status/util/xresources.py
Normal file
10
bumblebee_status/util/xresources.py
Normal 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")
|
||||
|
|
@ -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``.
|
||||
:)
|
||||
|
|
|
@ -8,4 +8,5 @@ Developer's Guide
|
|||
general
|
||||
module
|
||||
theme
|
||||
testing
|
||||
|
||||
|
|
|
@ -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 don’t 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 you’ve 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 that’s the case for you, you can stop reading now :)
|
||||
|
|
33
docs/development/testing.rst
Normal file
33
docs/development/testing.rst
Normal 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``.
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
176
docs/modules.rst
176
docs/modules.rst
|
@ -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)
|
||||
|
||||
|
@ -1284,6 +1398,9 @@ Parameters:
|
|||
* system.lock: specify a command for locking the screen (defaults to 'i3exit lock')
|
||||
* system.suspend: specify a command for suspending (defaults to 'i3exit suspend')
|
||||
* system.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!
|
||||
|
||||
|
@ -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
1
docs/requirements.txt
Normal file
|
@ -0,0 +1 @@
|
|||
docutils<0.18
|
1
requirements/modules/battery.txt
Normal file
1
requirements/modules/battery.txt
Normal file
|
@ -0,0 +1 @@
|
|||
power
|
|
@ -1 +1,2 @@
|
|||
dbus
|
||||
dbus-python
|
||||
power
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
requests
|
||||
Babel
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
dunst
|
|
@ -1 +0,0 @@
|
|||
hddtemp
|
1
requirements/modules/libvirtvms.txt
Normal file
1
requirements/modules/libvirtvms.txt
Normal file
|
@ -0,0 +1 @@
|
|||
libvirt-python
|
2
requirements/modules/octoprint.txt
Normal file
2
requirements/modules/octoprint.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Pillow
|
||||
simplejson
|
|
@ -1,3 +1 @@
|
|||
requests
|
||||
json
|
||||
time
|
||||
|
|
1
requirements/modules/speedtest.txt
Normal file
1
requirements/modules/speedtest.txt
Normal file
|
@ -0,0 +1 @@
|
|||
speedtest-cli
|
|
@ -1 +1 @@
|
|||
dbus
|
||||
dbus-python
|
||||
|
|
|
@ -1 +1 @@
|
|||
tkinter
|
||||
Pillow # placeholder for tk
|
||||
|
|
|
@ -1 +1 @@
|
|||
yubico
|
||||
python-yubico
|
||||
|
|
1
requirements/modules/zpool.txt
Normal file
1
requirements/modules/zpool.txt
Normal file
|
@ -0,0 +1 @@
|
|||
setuptools
|
BIN
screenshots/dunstctl.png
Normal file
BIN
screenshots/dunstctl.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 36 KiB |
BIN
screenshots/emerge_status.png
Normal file
BIN
screenshots/emerge_status.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.2 KiB |
BIN
screenshots/thunderbird.png
Normal file
BIN
screenshots/thunderbird.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
|
@ -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
|
||||
|
|
5
setup.py
5
setup.py
|
@ -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.*"])
|
||||
)
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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%-')
|
||||
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue