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
|
# Vim swap files
|
||||||
*swp
|
*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
|
language: python
|
||||||
|
env:
|
||||||
|
global:
|
||||||
|
- CC_TEST_REPORTER_ID=40cb00907f7a10e04868e856570bb997ab9c42fd3b63d980f2b2269433195fdf
|
||||||
python:
|
python:
|
||||||
- "3.4"
|
|
||||||
- "3.5"
|
|
||||||
- "3.6"
|
- "3.6"
|
||||||
- "3.7"
|
- "3.7"
|
||||||
before_install:
|
- "3.8"
|
||||||
- sudo apt-get -qq update
|
- "3.9"
|
||||||
|
before_script:
|
||||||
|
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
||||||
|
- chmod +x ./cc-test-reporter
|
||||||
|
- ./cc-test-reporter before-build
|
||||||
|
addons:
|
||||||
|
apt:
|
||||||
|
packages:
|
||||||
|
libdbus-1-dev
|
||||||
|
libgit2-dev
|
||||||
|
libvirt-dev
|
||||||
|
taskwarrior
|
||||||
install:
|
install:
|
||||||
- sudo apt-get install python-dbus
|
- pip install -U coverage pytest pytest-mock freezegun
|
||||||
- pip install -U coverage==4.3 pytest pytest-mock
|
- pip install 'pygit2<1' 'libvirt-python<6.3' 'feedparser<6' || true
|
||||||
- pip install codeclimate-test-reporter
|
- pip install $(cat requirements/modules/*.txt | cut -d ' ' -f 1 | sort -u)
|
||||||
- pip install i3-py Pillow Babel DateTime python-dateutil
|
|
||||||
- pip install docker feedparser i3ipc
|
|
||||||
- pip install netifaces power
|
|
||||||
- pip install psutil pytz
|
|
||||||
- pip install requests simplejson
|
|
||||||
- pip install suntime
|
|
||||||
- pip install tzlocal
|
|
||||||
script:
|
script:
|
||||||
- coverage run --source=. -m pytest tests -v
|
- coverage run --source=. -m pytest tests -v
|
||||||
- CODECLIMATE_REPO_TOKEN=40cb00907f7a10e04868e856570bb997ab9c42fd3b63d980f2b2269433195fdf codeclimate-test-reporter
|
after_script:
|
||||||
addons:
|
- coverage xml
|
||||||
code_climate:
|
- ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
|
||||||
repo_token: 40cb00907f7a10e04868e856570bb997ab9c42fd3b63d980f2b2269433195fdf
|
|
||||||
|
|
18
README.md
18
README.md
|
@ -1,6 +1,6 @@
|
||||||
# bumblebee-status
|
# bumblebee-status
|
||||||
|
|
||||||
[![Build Status](https://travis-ci.org/tobi-wan-kenobi/bumblebee-status.svg?branch=main)](https://travis-ci.org/tobi-wan-kenobi/bumblebee-status)
|
[![Build Status](https://app.travis-ci.com/tobi-wan-kenobi/bumblebee-status.svg?branch=main)](https://app.travis-ci.com/tobi-wan-kenobi/bumblebee-status)
|
||||||
[![Documentation Status](https://readthedocs.org/projects/bumblebee-status/badge/?version=main)](https://bumblebee-status.readthedocs.io/en/main/?badge=main)
|
[![Documentation Status](https://readthedocs.org/projects/bumblebee-status/badge/?version=main)](https://bumblebee-status.readthedocs.io/en/main/?badge=main)
|
||||||
![AUR version (release)](https://img.shields.io/aur/version/bumblebee-status)
|
![AUR version (release)](https://img.shields.io/aur/version/bumblebee-status)
|
||||||
![AUR version (git)](https://img.shields.io/aur/version/bumblebee-status-git)
|
![AUR version (git)](https://img.shields.io/aur/version/bumblebee-status-git)
|
||||||
|
@ -8,6 +8,8 @@
|
||||||
[![Code Climate](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/gpa.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status)
|
[![Code Climate](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/gpa.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status)
|
||||||
[![Test Coverage](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/coverage.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/coverage)
|
[![Test Coverage](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/coverage.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/coverage)
|
||||||
[![Issue Count](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/issue_count.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status)
|
[![Issue Count](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/issue_count.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status)
|
||||||
|
[![CodeQL](https://github.com/tobi-wan-kenobi/bumblebee-status/actions/workflows/codeql-analysis.yml/badge.svg?branch=main)](https://github.com/tobi-wan-kenobi/bumblebee-status/actions/workflows/codeql-analysis.yml)
|
||||||
|
![License](https://img.shields.io/github/license/tobi-wan-kenobi/bumblebee-status)
|
||||||
|
|
||||||
**Many, many thanks to all contributors! I am still amazed by and deeply grateful for how many PRs this project gets.**
|
**Many, many thanks to all contributors! I am still amazed by and deeply grateful for how many PRs this project gets.**
|
||||||
|
|
||||||
|
@ -28,16 +30,14 @@ Thanks a lot!
|
||||||
|
|
||||||
Required i3wm version: 4.12+ (in earlier versions, blocks won't have background colors)
|
Required i3wm version: 4.12+ (in earlier versions, blocks won't have background colors)
|
||||||
|
|
||||||
Supported Python versions: 3.4, 3.5, 3.6, 3.7, 3.8
|
Supported Python versions: 3.4, 3.5, 3.6, 3.7, 3.8, 3.9
|
||||||
|
|
||||||
Supported FontAwesome version: 4 (free version of 5 doesn't include some of the icons)
|
Supported FontAwesome version: 4 (free version of 5 doesn't include some of the icons)
|
||||||
|
|
||||||
---
|
---
|
||||||
**NOTE**
|
***NOTE***
|
||||||
|
|
||||||
The default branch for this project is `main` - I'm keeping `master` around for backwards compatibility (I do not want to break anybody's setup), but the default branch is now `main`!
|
The default branch for this project is `main`. If you are curious why: [ZDNet:github-master-alternative](https://www.zdnet.com/article/github-to-replace-master-with-alternative-term-to-avoid-slavery-references/)
|
||||||
|
|
||||||
If you are curious why: [ZDNet:github-master-alternative](https://www.zdnet.com/article/github-to-replace-master-with-alternative-term-to-avoid-slavery-references/)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -76,10 +76,16 @@ makepkg -sicr
|
||||||
pip install --user bumblebee-status
|
pip install --user bumblebee-status
|
||||||
```
|
```
|
||||||
|
|
||||||
|
There is also a SlackBuild available here: [slackbuilds:bumblebee-status](http://slackbuilds.org/repository/14.2/desktop/bumblebee-status/) - many thanks to [@Tonus1](https://github.com/Tonus1)!
|
||||||
|
|
||||||
|
An ebuild, for Gentoo Linux, is available on [gallifrey overlay](https://github.com/fedeliallalinea/gallifrey/tree/master/x11-misc/bumblebee-status). Instructions for adding the overlay can be found [here](https://github.com/fedeliallalinea/gallifrey/blob/master/README.md).
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
[Available modules](https://bumblebee-status.readthedocs.io/en/main/modules.html) lists the dependencies (Python modules and external executables)
|
[Available modules](https://bumblebee-status.readthedocs.io/en/main/modules.html) lists the dependencies (Python modules and external executables)
|
||||||
for each module. If you are not using a module, you don't need the dependencies.
|
for each module. If you are not using a module, you don't need the dependencies.
|
||||||
|
|
||||||
|
Some themes (e.g. all ‘powerline’ themes) require Font Awesome http://fontawesome.io/ and a powerline-compatible font (powerline-fonts) https://github.com/powerline/fonts
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
## Normal usage
|
## Normal usage
|
||||||
In your i3wm configuration, modify the *status_command* for your i3bar like this:
|
In your i3wm configuration, modify the *status_command* for your i3bar like this:
|
||||||
|
|
BIN
bin/get-kbd-layout
Executable file
BIN
bin/get-kbd-layout
Executable file
Binary file not shown.
|
@ -12,6 +12,7 @@ button = {
|
||||||
"right-mouse": 3,
|
"right-mouse": 3,
|
||||||
"wheel-up": 4,
|
"wheel-up": 4,
|
||||||
"wheel-down": 5,
|
"wheel-down": 5,
|
||||||
|
"update": -1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,7 +21,7 @@ def main():
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-b",
|
"-b",
|
||||||
"--button",
|
"--button",
|
||||||
choices=["left-mouse", "right-mouse", "middle-mouse", "wheel-up", "wheel-down"],
|
choices=["left-mouse", "right-mouse", "middle-mouse", "wheel-up", "wheel-down", "update"],
|
||||||
help="button to emulate",
|
help="button to emulate",
|
||||||
default="left-mouse",
|
default="left-mouse",
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
@ -6,7 +6,6 @@ import json
|
||||||
import time
|
import time
|
||||||
import signal
|
import signal
|
||||||
import socket
|
import socket
|
||||||
import select
|
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
@ -39,41 +38,40 @@ class CommandSocket(object):
|
||||||
self.__socket.close()
|
self.__socket.close()
|
||||||
os.unlink(self.__name)
|
os.unlink(self.__name)
|
||||||
|
|
||||||
|
def process_event(event_line, config, update_lock):
|
||||||
def handle_input(output, 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()
|
|
||||||
|
|
||||||
modules = {}
|
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:
|
try:
|
||||||
event = json.loads(line)
|
event = json.loads(event_line)
|
||||||
core.input.trigger(event)
|
core.input.trigger(event)
|
||||||
if "name" in event:
|
if "name" in event:
|
||||||
modules[event["name"]] = True
|
modules[event["name"]] = True
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
update_lock.acquire()
|
|
||||||
core.event.trigger("update", modules.keys())
|
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")
|
core.event.trigger("draw")
|
||||||
update_lock.release()
|
update_lock.release()
|
||||||
|
|
||||||
poll.unregister(sys.stdin.fileno())
|
def handle_commands(config, update_lock):
|
||||||
|
with CommandSocket() as cmdsocket:
|
||||||
|
while True:
|
||||||
|
tmp, _ = cmdsocket.accept()
|
||||||
|
line = tmp.recv(4096).decode()
|
||||||
|
tmp.close()
|
||||||
|
logging.debug("socket event {}".format(line))
|
||||||
|
process_event(line, config, update_lock)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_events(config, update_lock):
|
||||||
|
while True:
|
||||||
|
line = sys.stdin.readline().strip(",").strip()
|
||||||
|
if line == "[": continue
|
||||||
|
logging.info("input event: {}".format(line))
|
||||||
|
process_event(line, config, update_lock)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
@ -100,9 +98,13 @@ def main():
|
||||||
core.input.register(None, core.input.WHEEL_DOWN, "i3-msg workspace next_on_output")
|
core.input.register(None, core.input.WHEEL_DOWN, "i3-msg workspace next_on_output")
|
||||||
|
|
||||||
update_lock = threading.Lock()
|
update_lock = threading.Lock()
|
||||||
input_thread = threading.Thread(target=handle_input, args=(output, update_lock, ))
|
event_thread = threading.Thread(target=handle_events, args=(config, update_lock, ))
|
||||||
input_thread.daemon = True
|
event_thread.daemon = True
|
||||||
input_thread.start()
|
event_thread.start()
|
||||||
|
|
||||||
|
cmd_thread = threading.Thread(target=handle_commands, args=(config, update_lock, ))
|
||||||
|
cmd_thread.daemon = True
|
||||||
|
cmd_thread.start()
|
||||||
|
|
||||||
def sig_USR1_handler(signum,stack):
|
def sig_USR1_handler(signum,stack):
|
||||||
if update_lock.acquire(blocking=False) == True:
|
if update_lock.acquire(blocking=False) == True:
|
||||||
|
|
|
@ -147,6 +147,13 @@ class Config(util.store.Store):
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="bumblebee-status is a modular, theme-able status line generator for the i3 window manager. https://github.com/tobi-wan-kenobi/bumblebee-status/wiki"
|
description="bumblebee-status is a modular, theme-able status line generator for the i3 window manager. https://github.com/tobi-wan-kenobi/bumblebee-status/wiki"
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-c",
|
||||||
|
"--config-file",
|
||||||
|
action="store",
|
||||||
|
default=None,
|
||||||
|
help="Specify a configuration file to use"
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-m", "--modules", nargs="+", action="append", default=[], help=MODULE_HELP
|
"-m", "--modules", nargs="+", action="append", default=[], help=MODULE_HELP
|
||||||
)
|
)
|
||||||
|
@ -172,6 +179,13 @@ class Config(util.store.Store):
|
||||||
default=[],
|
default=[],
|
||||||
help="Specify a list of modules to hide when not in warning/error state",
|
help="Specify a list of modules to hide when not in warning/error state",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-e",
|
||||||
|
"--errorhide",
|
||||||
|
nargs="+",
|
||||||
|
default=[],
|
||||||
|
help="Specify a list of modules that are hidden when in state error"
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-d", "--debug", action="store_true", help="Add debug fields to i3 output"
|
"-d", "--debug", action="store_true", help="Add debug fields to i3 output"
|
||||||
)
|
)
|
||||||
|
@ -196,6 +210,11 @@ class Config(util.store.Store):
|
||||||
|
|
||||||
self.__args = parser.parse_args(args)
|
self.__args = parser.parse_args(args)
|
||||||
|
|
||||||
|
if self.__args.config_file:
|
||||||
|
cfg = self.__args.config_file
|
||||||
|
cfg = os.path.expanduser(cfg)
|
||||||
|
self.load_config(cfg)
|
||||||
|
else:
|
||||||
for cfg in [
|
for cfg in [
|
||||||
"~/.bumblebee-status.conf",
|
"~/.bumblebee-status.conf",
|
||||||
"~/.config/bumblebee-status.conf",
|
"~/.config/bumblebee-status.conf",
|
||||||
|
@ -302,14 +321,21 @@ class Config(util.store.Store):
|
||||||
def iconset(self):
|
def iconset(self):
|
||||||
return self.__args.iconset
|
return self.__args.iconset
|
||||||
|
|
||||||
"""Returns which modules should be hidden if their state is not warning/critical
|
"""Returns whether a module should be hidden if their state is not warning/critical
|
||||||
|
|
||||||
:return: list of modules to hide automatically
|
:return: True if module should be hidden automatically, False otherwise
|
||||||
:rtype: list of strings
|
:rtype: bool
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def autohide(self, name):
|
def autohide(self, name):
|
||||||
return name in self.__args.autohide
|
return name in self.__args.autohide or name in util.format.aslist(self.get("autohide", []))
|
||||||
|
|
||||||
|
"""Returns which modules should be hidden if they are in state error
|
||||||
|
|
||||||
|
:return: returns True if name should be hidden, False otherwise
|
||||||
|
:rtype: bool
|
||||||
|
"""
|
||||||
|
def errorhide(self, name):
|
||||||
|
return name in self.__args.errorhide
|
||||||
|
|
||||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
||||||
|
|
|
@ -8,6 +8,13 @@ def register(event, callback, *args, **kwargs):
|
||||||
|
|
||||||
__callbacks.setdefault(event, []).append(cb)
|
__callbacks.setdefault(event, []).append(cb)
|
||||||
|
|
||||||
|
def register_exclusive(event, callback, *args, **kwargs):
|
||||||
|
cb = callback
|
||||||
|
if args or kwargs:
|
||||||
|
cb = lambda: callback(*args, **kwargs)
|
||||||
|
|
||||||
|
__callbacks[event] = [cb]
|
||||||
|
|
||||||
def unregister(event):
|
def unregister(event):
|
||||||
if event in __callbacks:
|
if event in __callbacks:
|
||||||
del __callbacks[event]
|
del __callbacks[event]
|
||||||
|
|
|
@ -10,6 +10,7 @@ MIDDLE_MOUSE = 2
|
||||||
RIGHT_MOUSE = 3
|
RIGHT_MOUSE = 3
|
||||||
WHEEL_UP = 4
|
WHEEL_UP = 4
|
||||||
WHEEL_DOWN = 5
|
WHEEL_DOWN = 5
|
||||||
|
UPDATE = -1
|
||||||
|
|
||||||
|
|
||||||
def button_name(button):
|
def button_name(button):
|
||||||
|
@ -23,6 +24,8 @@ def button_name(button):
|
||||||
return "wheel-up"
|
return "wheel-up"
|
||||||
if button == WHEEL_DOWN:
|
if button == WHEEL_DOWN:
|
||||||
return "wheel-down"
|
return "wheel-down"
|
||||||
|
if button == UPDATE:
|
||||||
|
return "update"
|
||||||
return "n/a"
|
return "n/a"
|
||||||
|
|
||||||
|
|
||||||
|
@ -51,10 +54,13 @@ def register(obj, button=None, cmd=None, wait=False):
|
||||||
event_id = __event_id(obj.id if obj is not None else "", button)
|
event_id = __event_id(obj.id if obj is not None else "", button)
|
||||||
logging.debug("registering callback {}".format(event_id))
|
logging.debug("registering callback {}".format(event_id))
|
||||||
core.event.unregister(event_id) # make sure there's always only one input event
|
core.event.unregister(event_id) # make sure there's always only one input event
|
||||||
|
|
||||||
if callable(cmd):
|
if callable(cmd):
|
||||||
core.event.register(event_id, cmd)
|
core.event.register_exclusive(event_id, cmd)
|
||||||
|
elif obj and hasattr(obj, cmd) and callable(getattr(obj, cmd)):
|
||||||
|
core.event.register_exclusive(event_id, lambda event: getattr(obj, cmd)(event))
|
||||||
else:
|
else:
|
||||||
core.event.register(event_id, lambda event: __execute(event, cmd, wait))
|
core.event.register_exclusive(event_id, lambda event: __execute(event, cmd, wait))
|
||||||
|
|
||||||
|
|
||||||
def trigger(event):
|
def trigger(event):
|
||||||
|
|
|
@ -17,6 +17,27 @@ except Exception as e:
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def import_user(module_short, config, theme):
|
||||||
|
usermod = os.path.expanduser("~/.config/bumblebee-status/modules/{}.py".format(module_short))
|
||||||
|
if os.path.exists(usermod):
|
||||||
|
if hasattr(importlib, "machinery"):
|
||||||
|
log.debug("importing {} from user via machinery".format(module_short))
|
||||||
|
mod = importlib.machinery.SourceFileLoader("modules.{}".format(module_short),
|
||||||
|
os.path.expanduser(usermod)).load_module()
|
||||||
|
return getattr(mod, "Module")(config, theme)
|
||||||
|
else:
|
||||||
|
log.debug("importing {} from user via importlib.util".format(module_short))
|
||||||
|
try:
|
||||||
|
spec = importlib.util.spec_from_file_location("modules.{}".format(module_short), usermod)
|
||||||
|
mod = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(mod)
|
||||||
|
return mod.Module(config, theme)
|
||||||
|
except Exception as e:
|
||||||
|
spec = importlib.util.find_spec("modules.{}".format(module_short), usermod)
|
||||||
|
mod = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(mod)
|
||||||
|
return mod.Module(config, theme)
|
||||||
|
raise ImportError("not found")
|
||||||
|
|
||||||
"""Loads a module by name
|
"""Loads a module by name
|
||||||
|
|
||||||
|
@ -33,20 +54,25 @@ def load(module_name, config=core.config.Config([]), theme=None):
|
||||||
error = None
|
error = None
|
||||||
module_short, alias = (module_name.split(":") + [module_name])[0:2]
|
module_short, alias = (module_name.split(":") + [module_name])[0:2]
|
||||||
config.set("__alias__", alias)
|
config.set("__alias__", alias)
|
||||||
for namespace in ["core", "contrib"]:
|
|
||||||
try:
|
try:
|
||||||
mod = importlib.import_module(
|
mod = importlib.import_module("modules.core.{}".format(module_short))
|
||||||
"modules.{}.{}".format(namespace, module_short)
|
log.debug("importing {} from core".format(module_short))
|
||||||
)
|
|
||||||
log.debug(
|
|
||||||
"importing {} from {}.{}".format(module_short, namespace, module_short)
|
|
||||||
)
|
|
||||||
return getattr(mod, "Module")(config, theme)
|
return getattr(mod, "Module")(config, theme)
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
log.debug("failed to import {}: {}".format(module_name, e))
|
try:
|
||||||
error = e
|
log.warning("failed to import {} from core: {}".format(module_short, e))
|
||||||
log.fatal("failed to import {}: {}".format(module_name, error))
|
mod = importlib.import_module("modules.contrib.{}".format(module_short))
|
||||||
return Error(config=config, module=module_name, error=error)
|
log.debug("importing {} from contrib".format(module_short))
|
||||||
|
return getattr(mod, "Module")(config, theme)
|
||||||
|
except ImportError as e:
|
||||||
|
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):
|
class Module(core.input.Object):
|
||||||
|
@ -69,6 +95,8 @@ class Module(core.input.Object):
|
||||||
self.alias = self.__config.get("__alias__", None)
|
self.alias = self.__config.get("__alias__", None)
|
||||||
self.id = self.alias if self.alias else self.name
|
self.id = self.alias if self.alias else self.name
|
||||||
self.next_update = None
|
self.next_update = None
|
||||||
|
self.minimized = False
|
||||||
|
self.minimized = self.parameter("start-minimized", False)
|
||||||
|
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
|
|
||||||
|
@ -100,6 +128,8 @@ class Module(core.input.Object):
|
||||||
|
|
||||||
for prefix in [self.name, self.module_name, self.alias]:
|
for prefix in [self.name, self.module_name, self.alias]:
|
||||||
value = self.__config.get("{}.{}".format(prefix, key), value)
|
value = self.__config.get("{}.{}".format(prefix, key), value)
|
||||||
|
if self.minimized:
|
||||||
|
value = self.__config.get("{}.minimized.{}".format(prefix, key), value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
"""Set a parameter for this module
|
"""Set a parameter for this module
|
||||||
|
@ -123,7 +153,7 @@ class Module(core.input.Object):
|
||||||
|
|
||||||
def update_wrapper(self):
|
def update_wrapper(self):
|
||||||
if self.background == True:
|
if self.background == True:
|
||||||
if self.__thread and self.__thread.isAlive():
|
if self.__thread and self.__thread.is_alive():
|
||||||
return # skip this update interval
|
return # skip this update interval
|
||||||
self.__thread = threading.Thread(target=self.internal_update, args=(True,))
|
self.__thread = threading.Thread(target=self.internal_update, args=(True,))
|
||||||
self.__thread.start()
|
self.__thread.start()
|
||||||
|
@ -170,9 +200,9 @@ class Module(core.input.Object):
|
||||||
:rtype: bumblebee_status.widget.Widget
|
:rtype: bumblebee_status.widget.Widget
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def add_widget(self, full_text="", name=None):
|
def add_widget(self, full_text="", name=None, hidden=False):
|
||||||
widget_id = "{}::{}".format(self.name, len(self.widgets()))
|
widget_id = "{}::{}".format(self.name, len(self.widgets()))
|
||||||
widget = core.widget.Widget(full_text=full_text, name=name, widget_id=widget_id)
|
widget = core.widget.Widget(full_text=full_text, name=name, widget_id=widget_id, hidden=hidden)
|
||||||
self.widgets().append(widget)
|
self.widgets().append(widget)
|
||||||
widget.module = self
|
widget.module = self
|
||||||
return widget
|
return widget
|
||||||
|
|
|
@ -57,6 +57,9 @@ class block(object):
|
||||||
def set(self, key, value):
|
def set(self, key, value):
|
||||||
self.__attributes[key] = value
|
self.__attributes[key] = value
|
||||||
|
|
||||||
|
def get(self, key, default=None):
|
||||||
|
return self.__attributes.get(key, default)
|
||||||
|
|
||||||
def is_pango(self, attr):
|
def is_pango(self, attr):
|
||||||
if isinstance(attr, dict) and "pango" in attr:
|
if isinstance(attr, dict) and "pango" in attr:
|
||||||
return True
|
return True
|
||||||
|
@ -91,9 +94,17 @@ class block(object):
|
||||||
assign(self.__attributes, result, "background", "bg")
|
assign(self.__attributes, result, "background", "bg")
|
||||||
|
|
||||||
if "full_text" in self.__attributes:
|
if "full_text" in self.__attributes:
|
||||||
|
prefix = self.__pad(self.pangoize(self.__attributes.get("prefix")))
|
||||||
|
suffix = self.__pad(self.pangoize(self.__attributes.get("suffix")))
|
||||||
|
self.set("_prefix", prefix)
|
||||||
|
self.set("_suffix", suffix)
|
||||||
|
self.set("_raw", self.get("full_text"))
|
||||||
result["full_text"] = self.pangoize(result["full_text"])
|
result["full_text"] = self.pangoize(result["full_text"])
|
||||||
result["full_text"] = self.__format(self.__attributes["full_text"])
|
result["full_text"] = self.__format(self.__attributes["full_text"])
|
||||||
|
|
||||||
|
if "min-width" in self.__attributes and "padding" in self.__attributes:
|
||||||
|
self.set("min-width", self.__format(self.get("min-width")))
|
||||||
|
|
||||||
for k in [
|
for k in [
|
||||||
"name",
|
"name",
|
||||||
"instance",
|
"instance",
|
||||||
|
@ -123,11 +134,8 @@ class block(object):
|
||||||
def __format(self, text):
|
def __format(self, text):
|
||||||
if text is None:
|
if text is None:
|
||||||
return None
|
return None
|
||||||
prefix = self.__pad(self.pangoize(self.__attributes.get("prefix")))
|
prefix = self.get("_prefix")
|
||||||
suffix = self.__pad(self.pangoize(self.__attributes.get("suffix")))
|
suffix = self.get("_suffix")
|
||||||
self.set("_prefix", prefix)
|
|
||||||
self.set("_suffix", suffix)
|
|
||||||
self.set("_raw", text)
|
|
||||||
return "{}{}{}".format(prefix, text, suffix)
|
return "{}{}{}".format(prefix, text, suffix)
|
||||||
|
|
||||||
|
|
||||||
|
@ -158,6 +166,12 @@ class i3(object):
|
||||||
def toggle_minimize(self, event):
|
def toggle_minimize(self, event):
|
||||||
widget_id = event["instance"]
|
widget_id = event["instance"]
|
||||||
|
|
||||||
|
for module in self.__modules:
|
||||||
|
if module.widget(widget_id=widget_id) and util.format.asbool(module.parameter("minimize", False)) == True:
|
||||||
|
# this module can customly minimize
|
||||||
|
module.minimized = not module.minimized
|
||||||
|
return
|
||||||
|
|
||||||
if widget_id in self.__content:
|
if widget_id in self.__content:
|
||||||
self.__content[widget_id]["minimized"] = not self.__content[widget_id]["minimized"]
|
self.__content[widget_id]["minimized"] = not self.__content[widget_id]["minimized"]
|
||||||
|
|
||||||
|
@ -208,14 +222,22 @@ class i3(object):
|
||||||
|
|
||||||
def blocks(self, module):
|
def blocks(self, module):
|
||||||
blocks = []
|
blocks = []
|
||||||
|
if module.minimized:
|
||||||
|
blocks.extend(self.separator_block(module, module.widgets()[0]))
|
||||||
|
blocks.append(self.__content_block(module, module.widgets()[0]))
|
||||||
|
return blocks
|
||||||
for widget in module.widgets():
|
for widget in module.widgets():
|
||||||
if widget.module and self.__config.autohide(widget.module.name):
|
if widget.module and self.__config.autohide(widget.module.name):
|
||||||
if not any(
|
if not any(
|
||||||
state in widget.state() for state in ["warning", "critical"]
|
state in widget.state() for state in ["warning", "critical", "no-autohide"]
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
if module.hidden():
|
if module.hidden():
|
||||||
continue
|
continue
|
||||||
|
if widget.hidden:
|
||||||
|
continue
|
||||||
|
if "critical" in widget.state() and self.__config.errorhide(widget.module.name):
|
||||||
|
continue
|
||||||
blocks.extend(self.separator_block(module, widget))
|
blocks.extend(self.separator_block(module, widget))
|
||||||
blocks.append(self.__content_block(module, widget))
|
blocks.append(self.__content_block(module, widget))
|
||||||
core.event.trigger("next-widget")
|
core.event.trigger("next-widget")
|
||||||
|
|
|
@ -7,6 +7,7 @@ import glob
|
||||||
|
|
||||||
import core.event
|
import core.event
|
||||||
import util.algorithm
|
import util.algorithm
|
||||||
|
import util.xresources
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -16,6 +17,7 @@ PATHS = [
|
||||||
os.path.join(THEME_BASE_DIR, "../../themes"),
|
os.path.join(THEME_BASE_DIR, "../../themes"),
|
||||||
os.path.expanduser("~/.config/bumblebee-status/themes"),
|
os.path.expanduser("~/.config/bumblebee-status/themes"),
|
||||||
os.path.expanduser("~/.local/share/bumblebee-status/themes"), # PIP
|
os.path.expanduser("~/.local/share/bumblebee-status/themes"), # PIP
|
||||||
|
"/usr/share/bumblebee-status/themes",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -89,12 +91,20 @@ class Theme(object):
|
||||||
try:
|
try:
|
||||||
if isinstance(name, dict):
|
if isinstance(name, dict):
|
||||||
return name
|
return name
|
||||||
|
|
||||||
|
result = {}
|
||||||
if name.lower() == "wal":
|
if name.lower() == "wal":
|
||||||
wal = self.__load_json("~/.cache/wal/colors.json")
|
wal = self.__load_json("~/.cache/wal/colors.json")
|
||||||
result = {}
|
|
||||||
for field in ["special", "colors"]:
|
for field in ["special", "colors"]:
|
||||||
for key in wal.get(field, {}):
|
for key in wal.get(field, {}):
|
||||||
result[key] = wal[field][key]
|
result[key] = wal[field][key]
|
||||||
|
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
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error("failed to load colors: {}", e)
|
log.error("failed to load colors: {}", e)
|
||||||
|
|
|
@ -10,12 +10,13 @@ log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Widget(util.store.Store, core.input.Object):
|
class Widget(util.store.Store, core.input.Object):
|
||||||
def __init__(self, full_text="", name=None, widget_id=None):
|
def __init__(self, full_text="", name=None, widget_id=None, hidden=False):
|
||||||
super(Widget, self).__init__()
|
super(Widget, self).__init__()
|
||||||
self.__full_text = full_text
|
self.__full_text = full_text
|
||||||
self.module = None
|
self.module = None
|
||||||
self.name = name
|
self.name = name
|
||||||
self.id = widget_id or self.id
|
self.id = widget_id or self.id
|
||||||
|
self.hidden = hidden
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def module(self):
|
def module(self):
|
||||||
|
|
|
@ -14,6 +14,7 @@ import threading
|
||||||
import core.module
|
import core.module
|
||||||
import core.widget
|
import core.widget
|
||||||
import core.decorators
|
import core.decorators
|
||||||
|
import core.input
|
||||||
|
|
||||||
import util.cli
|
import util.cli
|
||||||
|
|
||||||
|
@ -56,6 +57,8 @@ class Module(core.module.Module):
|
||||||
def __init__(self, config, theme):
|
def __init__(self, config, theme):
|
||||||
super().__init__(config, theme, core.widget.Widget(self.updates))
|
super().__init__(config, theme, core.widget.Widget(self.updates))
|
||||||
self.__thread = None
|
self.__thread = None
|
||||||
|
core.input.register(self, button=core.input.RIGHT_MOUSE,
|
||||||
|
cmd=self.updates)
|
||||||
|
|
||||||
def updates(self, widget):
|
def updates(self, widget):
|
||||||
if widget.get("error"):
|
if widget.get("error"):
|
||||||
|
@ -65,7 +68,7 @@ class Module(core.module.Module):
|
||||||
)
|
)
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
if self.__thread and self.__thread.isAlive():
|
if self.__thread and self.__thread.is_alive():
|
||||||
return
|
return
|
||||||
|
|
||||||
self.__thread = threading.Thread(target=get_apt_check_info, args=(self,))
|
self.__thread = threading.Thread(target=get_apt_check_info, args=(self,))
|
||||||
|
|
|
@ -54,7 +54,7 @@ class Module(core.module.Module):
|
||||||
def activate_layout(layout_path):
|
def activate_layout(layout_path):
|
||||||
log.debug("activating layout")
|
log.debug("activating layout")
|
||||||
log.debug(layout_path)
|
log.debug(layout_path)
|
||||||
execute(layout_path)
|
execute(layout_path, ignore_errors=True)
|
||||||
|
|
||||||
def popup(self, widget):
|
def popup(self, widget):
|
||||||
"""Create Popup that allows the user to control their displays in one
|
"""Create Popup that allows the user to control their displays in one
|
||||||
|
@ -64,7 +64,7 @@ class Module(core.module.Module):
|
||||||
menu = popup.menu()
|
menu = popup.menu()
|
||||||
menu.add_menuitem(
|
menu.add_menuitem(
|
||||||
"arandr",
|
"arandr",
|
||||||
callback=partial(execute, self.manager)
|
callback=partial(execute, self.manager, ignore_errors=True)
|
||||||
)
|
)
|
||||||
menu.add_separator()
|
menu.add_separator()
|
||||||
|
|
||||||
|
@ -105,11 +105,12 @@ class Module(core.module.Module):
|
||||||
if count_on == 1:
|
if count_on == 1:
|
||||||
log.info("attempted to turn off last display")
|
log.info("attempted to turn off last display")
|
||||||
return
|
return
|
||||||
execute("{} --output {} --off".format(self.toggle_cmd, display))
|
execute("{} --output {} --off".format(self.toggle_cmd, display), ignore_errors=True)
|
||||||
else:
|
else:
|
||||||
log.debug("toggling on {}".format(display))
|
log.debug("toggling on {}".format(display))
|
||||||
execute(
|
execute(
|
||||||
"{} --output {} --auto".format(self.toggle_cmd, display)
|
"{} --output {} --auto".format(self.toggle_cmd, display),
|
||||||
|
ignore_errors=True
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -120,7 +121,7 @@ class Module(core.module.Module):
|
||||||
connected).
|
connected).
|
||||||
"""
|
"""
|
||||||
displays = {}
|
displays = {}
|
||||||
for line in execute("xrandr -q").split("\n"):
|
for line in execute("xrandr -q", ignore_errors=True).split("\n"):
|
||||||
if "connected" not in line:
|
if "connected" not in line:
|
||||||
continue
|
continue
|
||||||
is_on = bool(re.search(r"\d+x\d+\+(\d+)\+\d+", line))
|
is_on = bool(re.search(r"\d+x\d+\+(\d+)\+\d+", line))
|
||||||
|
@ -136,6 +137,7 @@ class Module(core.module.Module):
|
||||||
def _get_layouts():
|
def _get_layouts():
|
||||||
"""Loads and parses the arandr screen layout scripts."""
|
"""Loads and parses the arandr screen layout scripts."""
|
||||||
layouts = {}
|
layouts = {}
|
||||||
|
try:
|
||||||
for filename in os.listdir(__screenlayout_dir__):
|
for filename in os.listdir(__screenlayout_dir__):
|
||||||
if fnmatch.fnmatch(filename, '*.sh'):
|
if fnmatch.fnmatch(filename, '*.sh'):
|
||||||
fullpath = os.path.join(__screenlayout_dir__, filename)
|
fullpath = os.path.join(__screenlayout_dir__, filename)
|
||||||
|
@ -146,6 +148,8 @@ class Module(core.module.Module):
|
||||||
continue
|
continue
|
||||||
displays_in_file = Module._parse_layout(line)
|
displays_in_file = Module._parse_layout(line)
|
||||||
layouts[filename] = displays_in_file
|
layouts[filename] = displays_in_file
|
||||||
|
except Exception as e:
|
||||||
|
log.error(str(e))
|
||||||
return layouts
|
return layouts
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
@ -40,7 +40,7 @@ class Module(core.module.Module):
|
||||||
)
|
)
|
||||||
|
|
||||||
if code == 0:
|
if code == 0:
|
||||||
self.__packages = len(result.split("\n"))
|
self.__packages = len(result.strip().split("\n"))
|
||||||
elif code == 2:
|
elif code == 2:
|
||||||
self.__packages = 0
|
self.__packages = 0
|
||||||
else:
|
else:
|
||||||
|
|
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")
|
logging.debug("bt: toggling bluetooth")
|
||||||
util.cli.execute(cmd)
|
util.cli.execute(cmd, ignore_errors=True)
|
||||||
|
|
||||||
def state(self, widget):
|
def state(self, widget):
|
||||||
"""Get current state."""
|
"""Get current state."""
|
||||||
|
|
|
@ -69,7 +69,7 @@ class Module(core.module.Module):
|
||||||
)
|
)
|
||||||
|
|
||||||
logging.debug("bt: toggling bluetooth")
|
logging.debug("bt: toggling bluetooth")
|
||||||
core.util.execute(cmd)
|
util.cli.execute(cmd, ignore_errors=True)
|
||||||
|
|
||||||
def state(self, widget):
|
def state(self, widget):
|
||||||
"""Get current state."""
|
"""Get current state."""
|
||||||
|
|
|
@ -5,8 +5,6 @@ some media control bindings.
|
||||||
Left click toggles pause, scroll up skips the current song, scroll
|
Left click toggles pause, scroll up skips the current song, scroll
|
||||||
down returns to the previous song.
|
down returns to the previous song.
|
||||||
|
|
||||||
Requires the following library:
|
|
||||||
* subprocess
|
|
||||||
Parameters:
|
Parameters:
|
||||||
* deadbeef.format: Format string (defaults to '{artist} - {title}')
|
* deadbeef.format: Format string (defaults to '{artist} - {title}')
|
||||||
Available values are: {artist}, {title}, {album}, {length},
|
Available values are: {artist}, {title}, {album}, {length},
|
||||||
|
|
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 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
"""Fetch hard drive temeperature data from a hddtemp daemon
|
"""Fetch hard drive temperature data from a hddtemp daemon
|
||||||
that runs on localhost and default port (7634)
|
that runs on localhost and default port (7634)
|
||||||
|
|
||||||
contributed by `somospocos <https://github.com/somospocos>`_ - many thanks!
|
contributed by `somospocos <https://github.com/somospocos>`_ - many thanks!
|
||||||
|
|
|
@ -19,13 +19,13 @@ class Module(core.module.Module):
|
||||||
def __init__(self, config, theme):
|
def __init__(self, config, theme):
|
||||||
super().__init__(config, theme, core.widget.Widget(self.current_layout))
|
super().__init__(config, theme, core.widget.Widget(self.current_layout))
|
||||||
|
|
||||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__next_keymap)
|
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.next_keymap)
|
||||||
self.__current_layout = self.__get_current_layout()
|
self.__current_layout = self.__get_current_layout()
|
||||||
|
|
||||||
def current_layout(self, _):
|
def current_layout(self, _):
|
||||||
return self.__current_layout
|
return self.__current_layout
|
||||||
|
|
||||||
def __next_keymap(self, event):
|
def next_keymap(self, event):
|
||||||
util.cli.execute("xkb-switch -n", ignore_errors=True)
|
util.cli.execute("xkb-switch -n", ignore_errors=True)
|
||||||
|
|
||||||
def __get_current_layout(self):
|
def __get_current_layout(self):
|
||||||
|
|
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"""
|
"""Return default active network adapter"""
|
||||||
gateway = netifaces.gateways()["default"]
|
gateway = netifaces.gateways()["default"]
|
||||||
|
|
||||||
if not gateway:
|
|
||||||
raise "No default gateway found"
|
|
||||||
|
|
||||||
return gateway[netifaces.AF_INET][1]
|
return gateway[netifaces.AF_INET][1]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -4,11 +4,15 @@
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
* nvidiagpu.format: Format string (defaults to '{name}: {temp}°C %{usedmem}/{totalmem} MiB')
|
* nvidiagpu.format: Format string (defaults to '{name}: {temp}°C %{usedmem}/{totalmem} MiB')
|
||||||
Available values are: {name} {temp} {mem_used} {mem_total} {fanspeed} {clock_gpu} {clock_mem}
|
Available values are: {name} {temp} {mem_used} {mem_total} {fanspeed} {clock_gpu} {clock_mem} {gpu_usage_pct} {mem_usage_pct} {mem_io_pct}
|
||||||
|
|
||||||
Requires nvidia-smi
|
Requires nvidia-smi
|
||||||
|
|
||||||
contributed by `RileyRedpath <https://github.com/RileyRedpath>`_ - many thanks!
|
contributed by `RileyRedpath <https://github.com/RileyRedpath>`_ - many thanks!
|
||||||
|
|
||||||
|
Note: mem_io_pct is (from `man nvidia-smi`):
|
||||||
|
> Percent of time over the past sample period during which global (device)
|
||||||
|
> memory was being read or written.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import core.module
|
import core.module
|
||||||
|
@ -41,6 +45,9 @@ class Module(core.module.Module):
|
||||||
clockMem = ""
|
clockMem = ""
|
||||||
clockGpu = ""
|
clockGpu = ""
|
||||||
fanspeed = ""
|
fanspeed = ""
|
||||||
|
gpuUsagePct = ""
|
||||||
|
memIoPct = ""
|
||||||
|
memUsage = "not found"
|
||||||
for item in sp.split("\n"):
|
for item in sp.split("\n"):
|
||||||
try:
|
try:
|
||||||
key, val = item.split(":")
|
key, val = item.split(":")
|
||||||
|
@ -61,10 +68,18 @@ class Module(core.module.Module):
|
||||||
name = val
|
name = val
|
||||||
elif key == "Fan Speed":
|
elif key == "Fan Speed":
|
||||||
fanspeed = val.split(" ")[0]
|
fanspeed = val.split(" ")[0]
|
||||||
|
elif title == "Utilization":
|
||||||
|
if key == "Gpu":
|
||||||
|
gpuUsagePct = val.split(" ")[0]
|
||||||
|
elif key == "Memory":
|
||||||
|
memIoPct = val.split(" ")[0]
|
||||||
|
|
||||||
except:
|
except:
|
||||||
title = item.strip()
|
title = item.strip()
|
||||||
|
|
||||||
|
if totalMem and usedMem:
|
||||||
|
memUsage = int(int(usedMem) / int(totalMem) * 100)
|
||||||
|
|
||||||
str_format = self.parameter(
|
str_format = self.parameter(
|
||||||
"format", "{name}: {temp}°C {mem_used}/{mem_total} MiB"
|
"format", "{name}: {temp}°C {mem_used}/{mem_total} MiB"
|
||||||
)
|
)
|
||||||
|
@ -76,6 +91,9 @@ class Module(core.module.Module):
|
||||||
clock_gpu=clockGpu,
|
clock_gpu=clockGpu,
|
||||||
clock_mem=clockMem,
|
clock_mem=clockMem,
|
||||||
fanspeed=fanspeed,
|
fanspeed=fanspeed,
|
||||||
|
gpu_usage_pct=gpuUsagePct,
|
||||||
|
mem_io_pct=memIoPct,
|
||||||
|
mem_usage_pct=memUsage,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -85,8 +85,15 @@ class Module(core.module.Module):
|
||||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__show_popup)
|
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__show_popup)
|
||||||
|
|
||||||
def octoprint_status(self, widget):
|
def octoprint_status(self, widget):
|
||||||
if self.__octoprint_state == "Offline" or self.__octoprint_state == "Unknown":
|
if (
|
||||||
return self.__octoprint_state
|
self.__octoprint_state.startswith("Offline")
|
||||||
|
or self.__octoprint_state == "Unknown"
|
||||||
|
):
|
||||||
|
return (
|
||||||
|
(self.__octoprint_state[:25] + "...")
|
||||||
|
if len(self.__octoprint_state) > 25
|
||||||
|
else self.__octoprint_state
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
self.__octoprint_state
|
self.__octoprint_state
|
||||||
+ " | B: "
|
+ " | B: "
|
||||||
|
|
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:
|
Requires the following executable:
|
||||||
* playerctl
|
* playerctl
|
||||||
|
|
||||||
contributed by `smitajit <https://github.com/smitajit>`_ - many thanks!
|
Parameters:
|
||||||
|
* playerctl.format: Format string (defaults to '{{artist}} - {{title}} {{duration(position)}}/{{duration(mpris:length)}}').
|
||||||
|
The format string is passed to 'playerctl -f' as an argument. Read `the README <https://github.com/altdesktop/playerctl#printing-properties-and-metadata>`_ for more information.
|
||||||
|
* playerctl.layout: Comma-separated list to change order of widgets (defaults to song, previous, pause, next)
|
||||||
|
Widget names are: playerctl.song, playerctl.prev, playerctl.pause, playerctl.next
|
||||||
|
* playerctl.args: The arguments added to playerctl.
|
||||||
|
You can check 'playerctl --help' or `its README <https://github.com/altdesktop/playerctl#using-the-cli>`_. For example, it could be '-p vlc,%any'.
|
||||||
|
|
||||||
|
Parameters are inspired by the `spotify` module, many thanks to its developers!
|
||||||
|
|
||||||
|
contributed by `smitajit <https://github.com/smitajit>`_ - many thanks!
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import core.module
|
import core.module
|
||||||
import core.widget
|
import core.widget
|
||||||
import core.input
|
import core.input
|
||||||
import util.cli
|
import util.cli
|
||||||
|
import util.format
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
class Module(core.module.Module):
|
class Module(core.module.Module):
|
||||||
def __init__(self,config , theme):
|
def __init__(self, config, theme):
|
||||||
widgets = [
|
super(Module, self).__init__(config, theme, [])
|
||||||
core.widget.Widget(name="playerctl.prev"),
|
|
||||||
core.widget.Widget(name="playerctl.main", full_text=self.description),
|
self.background = True
|
||||||
core.widget.Widget(name="playerctl.next"),
|
|
||||||
|
self.__layout = util.format.aslist(
|
||||||
|
self.parameter(
|
||||||
|
"layout", "playerctl.prev, playerctl.song, playerctl.pause, playerctl.next"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
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",
|
||||||
|
}
|
||||||
]
|
]
|
||||||
super(Module, self).__init__(config, theme , widgets)
|
else:
|
||||||
|
raise KeyError(
|
||||||
|
"The playerctl module does not have a {widget_name!r} widget".format(
|
||||||
|
widget_name=widget_name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
core.input.register(widgets[0], button=core.input.LEFT_MOUSE,
|
for widget, callback_options in widget_map.items():
|
||||||
cmd="playerctl previous")
|
if isinstance(callback_options, dict):
|
||||||
core.input.register(widgets[1], button=core.input.LEFT_MOUSE,
|
core.input.register(widget, **callback_options)
|
||||||
cmd="playerctl play-pause")
|
|
||||||
core.input.register(widgets[2], button=core.input.LEFT_MOUSE,
|
|
||||||
cmd="playerctl next")
|
|
||||||
|
|
||||||
self._status = None
|
|
||||||
self._tags = None
|
|
||||||
|
|
||||||
def description(self, widget):
|
|
||||||
return self._tags if self._tags else "..."
|
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
self._load_song()
|
|
||||||
|
|
||||||
def state(self, widget):
|
|
||||||
if widget.name == "playerctl.prev":
|
|
||||||
return "prev"
|
|
||||||
if widget.name == "playerctl.next":
|
|
||||||
return "next"
|
|
||||||
return self._status
|
|
||||||
|
|
||||||
def _load_song(self):
|
|
||||||
info = ""
|
|
||||||
try:
|
try:
|
||||||
status = util.cli.execute("playerctl status").lower()
|
playback_status = str(util.cli.execute(self.__cmd + "status 2>&1 || true", shell = True)).strip()
|
||||||
info = util.cli.execute("playerctl metadata xesam:title")
|
if playback_status == "No players found":
|
||||||
except :
|
playback_status = None
|
||||||
self._status = None
|
except Exception as e:
|
||||||
self._tags = None
|
logging.exception(e)
|
||||||
return
|
playback_status = None
|
||||||
self._status = status.split("\n")[0].lower()
|
for widget in self.widgets():
|
||||||
self._tags = info.split("\n")[0][:20]
|
if playback_status:
|
||||||
|
if widget.name == "playerctl.pause":
|
||||||
|
if playback_status == "Playing":
|
||||||
|
widget.set("state", "playing")
|
||||||
|
elif playback_status == "Paused":
|
||||||
|
widget.set("state", "paused")
|
||||||
|
elif playback_status == "Stopped":
|
||||||
|
widget.set("state", "stopped")
|
||||||
|
else:
|
||||||
|
widget.set("state", "")
|
||||||
|
elif widget.name == "playerctl.next":
|
||||||
|
widget.set("state", "next")
|
||||||
|
elif widget.name == "playerctl.prev":
|
||||||
|
widget.set("state", "prev")
|
||||||
|
elif widget.name == "playerctl.song":
|
||||||
|
widget.full_text(self.__get_song())
|
||||||
|
else:
|
||||||
|
widget.set("state", "")
|
||||||
|
widget.full_text(" ")
|
||||||
|
|
||||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
def __get_song(self):
|
||||||
|
try:
|
||||||
|
return str(util.cli.execute(self.__cmd + "metadata -f '" + self.__format + "'")).strip()
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception(e)
|
||||||
|
return " "
|
||||||
|
|
|
@ -101,7 +101,7 @@ class Module(core.module.Module):
|
||||||
|
|
||||||
def state(self, widget):
|
def state(self, widget):
|
||||||
if self.__active:
|
if self.__active:
|
||||||
return "copying"
|
return ["copying", "no-autohide"]
|
||||||
return "pending"
|
return "pending"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -16,13 +16,13 @@ class Module(core.module.Module):
|
||||||
self.__ip = ""
|
self.__ip = ""
|
||||||
|
|
||||||
def public_ip(self, widget):
|
def public_ip(self, widget):
|
||||||
return self.__ip
|
return self.__ip or "n/a"
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
try:
|
try:
|
||||||
self.__ip = util.location.public_ip()
|
self.__ip = util.location.public_ip()
|
||||||
except Exception:
|
except Exception:
|
||||||
self.__ip = "n/a"
|
self.__ip = None
|
||||||
|
|
||||||
|
|
||||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
||||||
|
|
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
|
orientation = curr_orient
|
||||||
break
|
break
|
||||||
|
|
||||||
widget = self.widget(display)
|
widget = self.widget(name=display)
|
||||||
if not widget:
|
if not widget:
|
||||||
widget = self.add_widget(full_text=display, name=display)
|
widget = self.add_widget(full_text=display, name=display)
|
||||||
core.input.register(
|
core.input.register(
|
||||||
widget, button=core.input.LEFT_MOUSE, cmd=self.__toggle
|
widget, button=core.input.LEFT_MOUSE, cmd=self.__toggle
|
||||||
)
|
)
|
||||||
widget.set("orientation", orientation)
|
widget.set("orientation", orientation)
|
||||||
widgets.append(widget)
|
|
||||||
|
|
||||||
def state(self, widget):
|
def state(self, widget):
|
||||||
return widget.get("orientation", "normal")
|
return widget.get("orientation", "normal")
|
||||||
|
|
|
@ -55,7 +55,7 @@ class Module(core.module.Module):
|
||||||
|
|
||||||
self._state = []
|
self._state = []
|
||||||
|
|
||||||
self._newspaper_filename = tempfile.mktemp(".html")
|
self._newspaper_file = tempfile.NamedTemporaryFile(mode="w", suffix=".html")
|
||||||
|
|
||||||
self._last_refresh = 0
|
self._last_refresh = 0
|
||||||
self._last_update = 0
|
self._last_update = 0
|
||||||
|
@ -308,10 +308,11 @@ class Module(core.module.Module):
|
||||||
|
|
||||||
while newspaper_items:
|
while newspaper_items:
|
||||||
content += self._create_news_section(newspaper_items)
|
content += self._create_news_section(newspaper_items)
|
||||||
open(self._newspaper_filename, "w").write(
|
self._newspaper_file.write(
|
||||||
HTML_TEMPLATE.replace("[[CONTENT]]", content)
|
HTML_TEMPLATE.replace("[[CONTENT]]", content)
|
||||||
)
|
)
|
||||||
webbrowser.open("file://" + self._newspaper_filename)
|
self._newspaper_file.flush()
|
||||||
|
webbrowser.open("file://" + self._newspaper_file.name)
|
||||||
self._update_history("newspaper")
|
self._update_history("newspaper")
|
||||||
self._save_history()
|
self._save_history()
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
"""Displays sensor temperature
|
"""Displays sensor temperature
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
|
* sensors.use_sensors: whether to use the sensors command
|
||||||
* sensors.path: path to temperature file (default /sys/class/thermal/thermal_zone0/temp).
|
* sensors.path: path to temperature file (default /sys/class/thermal/thermal_zone0/temp).
|
||||||
* sensors.json: if set to 'true', interpret sensors.path as JSON 'path' in the output
|
* sensors.json: if set to 'true', interpret sensors.path as JSON 'path' in the output
|
||||||
of 'sensors -j' (i.e. <key1>/<key2>/.../<value>), for example, path could
|
of 'sensors -j' (i.e. <key1>/<key2>/.../<value>), for example, path could
|
||||||
|
@ -18,6 +19,7 @@ contributed by `mijoharas <https://github.com/mijoharas>`_ - many thanks!
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
import os
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
@ -46,22 +48,25 @@ class Module(core.module.Module):
|
||||||
self._json = util.format.asbool(self.parameter("json", False))
|
self._json = util.format.asbool(self.parameter("json", False))
|
||||||
self._freq = util.format.asbool(self.parameter("show_freq", True))
|
self._freq = util.format.asbool(self.parameter("show_freq", True))
|
||||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd="xsensors")
|
core.input.register(self, button=core.input.LEFT_MOUSE, cmd="xsensors")
|
||||||
self.determine_method()
|
self.use_sensors = self.determine_method()
|
||||||
|
|
||||||
def determine_method(self):
|
def determine_method(self):
|
||||||
|
if util.format.asbool(self.parameter("use_sensors")) == True:
|
||||||
|
return True
|
||||||
|
if util.format.asbool(self.parameter("use_sensors")) == False:
|
||||||
|
return False
|
||||||
if self.parameter("path") != None and self._json == False:
|
if self.parameter("path") != None and self._json == False:
|
||||||
self.use_sensors = False # use thermal zone
|
return False
|
||||||
else:
|
|
||||||
# try to use output of sensors -u
|
# try to use output of sensors -u
|
||||||
try:
|
try:
|
||||||
output = util.cli.execute("sensors -u")
|
_ = util.cli.execute("sensors -u")
|
||||||
self.use_sensors = True
|
|
||||||
log.debug("Sensors command available")
|
log.debug("Sensors command available")
|
||||||
|
return True
|
||||||
except FileNotFoundError as e:
|
except FileNotFoundError as e:
|
||||||
log.info(
|
log.info(
|
||||||
"Sensors command not available, using /sys/class/thermal/thermal_zone*/"
|
"Sensors command not available, using /sys/class/thermal/thermal_zone*/"
|
||||||
)
|
)
|
||||||
self.use_sensors = False
|
return False
|
||||||
|
|
||||||
def _get_temp_from_sensors(self):
|
def _get_temp_from_sensors(self):
|
||||||
if self._json == True:
|
if self._json == True:
|
||||||
|
@ -92,22 +97,31 @@ class Module(core.module.Module):
|
||||||
|
|
||||||
def get_temp(self):
|
def get_temp(self):
|
||||||
if self.use_sensors:
|
if self.use_sensors:
|
||||||
temperature = self._get_temp_from_sensors()
|
|
||||||
log.debug("Retrieve temperature from sensors -u")
|
log.debug("Retrieve temperature from sensors -u")
|
||||||
else:
|
return self._get_temp_from_sensors()
|
||||||
try:
|
try:
|
||||||
temperature = open(
|
path = None
|
||||||
self.parameter("path", "/sys/class/thermal/thermal_zone0/temp")
|
# use path provided by the user
|
||||||
).read()[:2]
|
if self.parameter("path") is not None:
|
||||||
log.debug("retrieved temperature from /sys/class/")
|
path = self.parameter("path")
|
||||||
# TODO: Iterate through all thermal zones to determine the correct one and use its value
|
# find the thermal zone that provides cpu temperature
|
||||||
# https://unix.stackexchange.com/questions/304845/discrepancy-between-number-of-cores-and-thermal-zones-in-sys-class-thermal
|
else:
|
||||||
|
for zone in os.listdir("/sys/class/thermal"):
|
||||||
|
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:
|
except IOError:
|
||||||
temperature = "unknown"
|
|
||||||
log.info("Can not determine temperature, please install lm-sensors")
|
log.info("Can not determine temperature, please install lm-sensors")
|
||||||
|
return "unknown"
|
||||||
return temperature
|
|
||||||
|
|
||||||
def get_mhz(self):
|
def get_mhz(self):
|
||||||
mhz = None
|
mhz = None
|
||||||
|
|
|
@ -47,13 +47,13 @@ class Module(core.module.Module):
|
||||||
self.__output = "please wait..."
|
self.__output = "please wait..."
|
||||||
self.__current_thread = threading.Thread()
|
self.__current_thread = threading.Thread()
|
||||||
|
|
||||||
# LMB and RMB will update output regardless of timer
|
if self.parameter("scrolling.makewide") is None:
|
||||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.update)
|
self.set("scrolling.makewide", False)
|
||||||
core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self.update)
|
|
||||||
|
|
||||||
def set_output(self, value):
|
def set_output(self, value):
|
||||||
self.__output = value
|
self.__output = value
|
||||||
|
|
||||||
|
@core.decorators.scrollable
|
||||||
def get_output(self, _):
|
def get_output(self, _):
|
||||||
return self.__output
|
return self.__output
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,12 @@
|
||||||
when clicking on it.
|
when clicking on it.
|
||||||
|
|
||||||
For more than one shortcut, the commands and labels are strings separated by
|
For more than one shortcut, the commands and labels are strings separated by
|
||||||
a demiliter (; semicolon by default).
|
a delimiter (; semicolon by default).
|
||||||
|
|
||||||
For example in order to create two shortcuts labeled A and B with commands
|
For example in order to create two shortcuts labeled A and B with commands
|
||||||
cmdA and cmdB you could do:
|
cmdA and cmdB you could do:
|
||||||
|
|
||||||
./bumblebee-status -m shortcut -p shortcut.cmd='ls;ps' shortcut.label='A;B'
|
./bumblebee-status -m shortcut -p shortcut.cmd='firefox https://www.google.com;google-chrome https://google.com' shortcut.label='Google (Firefox);Google (Chrome)'
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
* shortcut.cmds : List of commands to execute
|
* shortcut.cmds : List of commands to execute
|
||||||
|
|
|
@ -10,7 +10,7 @@ Requires the following executables:
|
||||||
* smartctl
|
* smartctl
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
* smartstatus.display: how to display (defaults to 'combined', other choices: 'seperate' or 'singles')
|
* smartstatus.display: how to display (defaults to 'combined', other choices: 'combined_singles', 'seperate' or 'singles')
|
||||||
* smartstatus.drives: in the case of singles which drives to display, separated comma list value, multiple accepted (defaults to 'sda', example:'sda,sdc')
|
* smartstatus.drives: in the case of singles which drives to display, separated comma list value, multiple accepted (defaults to 'sda', example:'sda,sdc')
|
||||||
* smartstatus.show_names: boolean in the form of "True" or "False" to show the name of the drives in the form of sda, sbd, combined or none at all.
|
* smartstatus.show_names: boolean in the form of "True" or "False" to show the name of the drives in the form of sda, sbd, combined or none at all.
|
||||||
"""
|
"""
|
||||||
|
@ -38,7 +38,7 @@ class Module(core.module.Module):
|
||||||
self.create_widgets()
|
self.create_widgets()
|
||||||
|
|
||||||
def create_widgets(self):
|
def create_widgets(self):
|
||||||
if self.display == "combined":
|
if self.display == "combined" or self.display == "combined_singles":
|
||||||
widget = self.add_widget()
|
widget = self.add_widget()
|
||||||
widget.set("device", "combined")
|
widget.set("device", "combined")
|
||||||
widget.set("assessment", self.combined())
|
widget.set("assessment", self.combined())
|
||||||
|
@ -81,6 +81,8 @@ class Module(core.module.Module):
|
||||||
|
|
||||||
def combined(self):
|
def combined(self):
|
||||||
for device in self.devices:
|
for device in self.devices:
|
||||||
|
if self.display == "combined_singles" and device not in self.drives:
|
||||||
|
continue
|
||||||
result = self.smart(device)
|
result = self.smart(device)
|
||||||
if result == "Fail":
|
if result == "Fail":
|
||||||
return "Fail"
|
return "Fail"
|
||||||
|
|
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:
|
Requires the following libraries:
|
||||||
* requests
|
* requests
|
||||||
* regex
|
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
* spaceapi.url: String representation of the api endpoint
|
* spaceapi.url: String representation of the api endpoint
|
||||||
|
|
|
@ -8,6 +8,10 @@ Parameters:
|
||||||
Available values are: {album}, {title}, {artist}, {trackNumber}
|
Available values are: {album}, {title}, {artist}, {trackNumber}
|
||||||
* spotify.layout: Comma-separated list to change order of widgets (defaults to song, previous, pause, next)
|
* spotify.layout: Comma-separated list to change order of widgets (defaults to song, previous, pause, next)
|
||||||
Widget names are: spotify.song, spotify.prev, spotify.pause, spotify.next
|
Widget names are: spotify.song, spotify.prev, spotify.pause, spotify.next
|
||||||
|
* spotify.concise_controls: When enabled, allows spotify to be controlled from just the spotify.song widget.
|
||||||
|
Concise controls are: Left Click: Toggle Pause; Wheel Up: Next; Wheel Down; Previous.
|
||||||
|
* spotify.bus_name: String (defaults to `spotify`)
|
||||||
|
Available values: spotify, spotifyd
|
||||||
|
|
||||||
contributed by `yvesh <https://github.com/yvesh>`_ - many thanks!
|
contributed by `yvesh <https://github.com/yvesh>`_ - many thanks!
|
||||||
|
|
||||||
|
@ -25,46 +29,34 @@ import core.input
|
||||||
import core.decorators
|
import core.decorators
|
||||||
import util.format
|
import util.format
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
class Module(core.module.Module):
|
class Module(core.module.Module):
|
||||||
def __init__(self, config, theme):
|
def __init__(self, config, theme):
|
||||||
super().__init__(config, theme, [])
|
super().__init__(config, theme, [])
|
||||||
|
|
||||||
|
self.background = True
|
||||||
|
|
||||||
|
self.__bus_name = self.parameter("bus_name", "spotify")
|
||||||
|
|
||||||
self.__layout = util.format.aslist(
|
self.__layout = util.format.aslist(
|
||||||
self.parameter(
|
self.parameter(
|
||||||
"layout", "spotify.song,spotify.prev,spotify.pause,spotify.next",
|
"layout", "spotify.song,spotify.prev,spotify.pause,spotify.next",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.__bus = dbus.SessionBus()
|
||||||
self.__song = ""
|
self.__song = ""
|
||||||
self.__pause = ""
|
self.__pause = ""
|
||||||
self.__format = self.parameter("format", "{artist} - {title}")
|
self.__format = self.parameter("format", "{artist} - {title}")
|
||||||
|
|
||||||
|
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 \
|
self.__cmd = "dbus-send --session --type=method_call --dest=org.mpris.MediaPlayer2.spotify \
|
||||||
/org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player."
|
/org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player."
|
||||||
|
|
||||||
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"
|
|
||||||
)
|
|
||||||
spotify_iface = dbus.Interface(spotify, "org.freedesktop.DBus.Properties")
|
|
||||||
props = spotify_iface.Get("org.mpris.MediaPlayer2.Player", "Metadata")
|
|
||||||
self.__song = self.__format.format(
|
|
||||||
album=str(props.get("xesam:album")),
|
|
||||||
title=str(props.get("xesam:title")),
|
|
||||||
artist=",".join(props.get("xesam:artist")),
|
|
||||||
trackNumber=str(props.get("xesam:trackNumber")),
|
|
||||||
)
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
try:
|
|
||||||
self.clear_widgets()
|
|
||||||
self.__get_song()
|
|
||||||
|
|
||||||
widget_map = {}
|
widget_map = {}
|
||||||
for widget_name in self.__layout:
|
for widget_name in self.__layout:
|
||||||
widget = self.add_widget(name=widget_name)
|
widget = self.add_widget(name=widget_name)
|
||||||
|
@ -79,19 +71,6 @@ class Module(core.module.Module):
|
||||||
"button": core.input.LEFT_MOUSE,
|
"button": core.input.LEFT_MOUSE,
|
||||||
"cmd": self.__cmd + "PlayPause",
|
"cmd": self.__cmd + "PlayPause",
|
||||||
}
|
}
|
||||||
playback_status = str(
|
|
||||||
dbus.Interface(
|
|
||||||
dbus.SessionBus().get_object(
|
|
||||||
"org.mpris.MediaPlayer2.spotify",
|
|
||||||
"/org/mpris/MediaPlayer2",
|
|
||||||
),
|
|
||||||
"org.freedesktop.DBus.Properties",
|
|
||||||
).Get("org.mpris.MediaPlayer2.Player", "PlaybackStatus")
|
|
||||||
)
|
|
||||||
if playback_status == "Playing":
|
|
||||||
widget.set("state", "playing")
|
|
||||||
else:
|
|
||||||
widget.set("state", "paused")
|
|
||||||
elif widget_name == "spotify.next":
|
elif widget_name == "spotify.next":
|
||||||
widget_map[widget] = {
|
widget_map[widget] = {
|
||||||
"button": core.input.LEFT_MOUSE,
|
"button": core.input.LEFT_MOUSE,
|
||||||
|
@ -99,18 +78,87 @@ class Module(core.module.Module):
|
||||||
}
|
}
|
||||||
widget.set("state", "next")
|
widget.set("state", "next")
|
||||||
elif widget_name == "spotify.song":
|
elif widget_name == "spotify.song":
|
||||||
widget.set("state", "song")
|
if util.format.asbool(self.parameter("concise_controls", "false")):
|
||||||
widget.full_text(self.__song)
|
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:
|
else:
|
||||||
raise KeyError(
|
raise KeyError(
|
||||||
"The spotify module does not have a {widget_name!r} widget".format(
|
"The spotify module does not have a {widget_name!r} widget".format(
|
||||||
widget_name=widget_name
|
widget_name=widget_name
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
# is there any reason the inputs can't be directly registered above?
|
||||||
for widget, callback_options in widget_map.items():
|
for widget, callback_options in widget_map.items():
|
||||||
|
if isinstance(callback_options, dict):
|
||||||
core.input.register(widget, **callback_options)
|
core.input.register(widget, **callback_options)
|
||||||
|
|
||||||
except Exception:
|
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 = 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(
|
||||||
|
album=str(props.get("xesam:album")),
|
||||||
|
title=str(props.get("xesam:title")),
|
||||||
|
artist=",".join(props.get("xesam:artist")),
|
||||||
|
trackNumber=str(props.get("xesam:trackNumber")),
|
||||||
|
)
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
try:
|
||||||
|
self.__get_song()
|
||||||
|
|
||||||
|
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(
|
||||||
|
bus,
|
||||||
|
"org.freedesktop.DBus.Properties",
|
||||||
|
).Get("org.mpris.MediaPlayer2.Player", "PlaybackStatus")
|
||||||
|
)
|
||||||
|
if playback_status == "Playing":
|
||||||
|
widget.set("state", "playing")
|
||||||
|
else:
|
||||||
|
widget.set("state", "paused")
|
||||||
|
elif widget.name == "spotify.song":
|
||||||
|
widget.set("state", "song")
|
||||||
|
widget.full_text(self.__song)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
self.__song = ""
|
self.__song = ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -3,9 +3,6 @@
|
||||||
|
|
||||||
"""Display a stock quote from finance.yahoo.com
|
"""Display a stock quote from finance.yahoo.com
|
||||||
|
|
||||||
Requires the following python packages:
|
|
||||||
* requests
|
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
* stock.symbols : Comma-separated list of symbols to fetch
|
* stock.symbols : Comma-separated list of symbols to fetch
|
||||||
* stock.change : Should we fetch change in stock value (defaults to True)
|
* stock.change : Should we fetch change in stock value (defaults to True)
|
||||||
|
|
|
@ -8,8 +8,8 @@ Requires the following python packages:
|
||||||
* python-dateutil
|
* python-dateutil
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
* cpu.lat : Latitude of your location
|
* sun.lat : Latitude of your location
|
||||||
* cpu.lon : Longitude of your location
|
* sun.lon : Longitude of your location
|
||||||
|
|
||||||
(if none of those are set, location is determined automatically via location APIs)
|
(if none of those are set, location is determined automatically via location APIs)
|
||||||
|
|
||||||
|
@ -39,7 +39,11 @@ class Module(core.module.Module):
|
||||||
self.__sun = None
|
self.__sun = None
|
||||||
|
|
||||||
if not lat or not lon:
|
if not lat or not lon:
|
||||||
|
try:
|
||||||
lat, lon = util.location.coordinates()
|
lat, lon = util.location.coordinates()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
if lat and lon:
|
if lat and lon:
|
||||||
self.__sun = Sun(float(lat), float(lon))
|
self.__sun = Sun(float(lat), float(lon))
|
||||||
|
|
||||||
|
@ -55,6 +59,10 @@ class Module(core.module.Module):
|
||||||
return "n/a"
|
return "n/a"
|
||||||
|
|
||||||
def __calculate_times(self):
|
def __calculate_times(self):
|
||||||
|
if not self.__sun:
|
||||||
|
self.__sunset = self.__sunrise = None
|
||||||
|
return
|
||||||
|
|
||||||
self.__isup = False
|
self.__isup = False
|
||||||
|
|
||||||
order_matters = True
|
order_matters = True
|
||||||
|
|
|
@ -21,6 +21,9 @@ Parameters:
|
||||||
* system.suspend: specify a command for suspending (defaults to 'i3exit suspend')
|
* system.suspend: specify a command for suspending (defaults to 'i3exit suspend')
|
||||||
* system.hibernate: specify a command for hibernating (defaults to 'i3exit hibernate')
|
* system.hibernate: specify a command for hibernating (defaults to 'i3exit hibernate')
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
tkinter (python3-tk package on debian based systems either you can install it as python package)
|
||||||
|
|
||||||
contributed by `bbernhard <https://github.com/bbernhard>`_ - many thanks!
|
contributed by `bbernhard <https://github.com/bbernhard>`_ - many thanks!
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
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))
|
super().__init__(config, theme, core.widget.Widget(self.output))
|
||||||
|
|
||||||
self.__doc = os.path.expanduser(self.parameter("file", "~/Documents/todo.txt"))
|
self.__doc = os.path.expanduser(self.parameter("file", "~/Documents/todo.txt"))
|
||||||
|
self.__editor = self.parameter("editor", "xdg-open")
|
||||||
self.__todos = self.count_items()
|
self.__todos = self.count_items()
|
||||||
core.input.register(
|
core.input.register(
|
||||||
self, button=core.input.LEFT_MOUSE, cmd="xdg-open {}".format(self.__doc)
|
self, button=core.input.LEFT_MOUSE, cmd="{} {}".format(self.__editor, self.__doc)
|
||||||
)
|
)
|
||||||
|
|
||||||
def output(self, widget):
|
def output(self, widget):
|
||||||
|
@ -39,11 +40,12 @@ class Module(core.module.Module):
|
||||||
|
|
||||||
def count_items(self):
|
def count_items(self):
|
||||||
try:
|
try:
|
||||||
i = -1
|
i = 0
|
||||||
with open(self.__doc) as f:
|
with open(self.__doc) as f:
|
||||||
for i, l in enumerate(f):
|
for l in f.readlines():
|
||||||
pass
|
if l.strip() != '':
|
||||||
return i + 1
|
i += 1
|
||||||
|
return i
|
||||||
except Exception:
|
except Exception:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
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.warning : Warning threshold in % of CPU usage (defaults to 70%)
|
||||||
* cpu.critical: Critical threshold in % of CPU usage (defaults to 80%)
|
* cpu.critical: Critical threshold in % of CPU usage (defaults to 80%)
|
||||||
* cpu.format : Format string (defaults to '{:.01f}%')
|
* cpu.format : Format string (defaults to '{:.01f}%')
|
||||||
|
* cpu.percpu : If set to true, show each individual cpu (defaults to false)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import psutil
|
import psutil
|
||||||
|
@ -20,12 +21,19 @@ import core.module
|
||||||
import core.widget
|
import core.widget
|
||||||
import core.input
|
import core.input
|
||||||
|
|
||||||
|
import util.format
|
||||||
|
|
||||||
|
|
||||||
class Module(core.module.Module):
|
class Module(core.module.Module):
|
||||||
def __init__(self, config, theme):
|
def __init__(self, config, theme):
|
||||||
super().__init__(config, theme, core.widget.Widget(self.utilization))
|
super().__init__(config, theme, [])
|
||||||
self.widget().set("theme.minwidth", self._format.format(100.0 - 10e-20))
|
self._percpu = util.format.asbool(self.parameter("percpu", False))
|
||||||
self._utilization = psutil.cpu_percent(percpu=False)
|
|
||||||
|
for idx, cpu_perc in enumerate(self.cpu_utilization()):
|
||||||
|
widget = self.add_widget(name="cpu#{}".format(idx), full_text=self.utilization)
|
||||||
|
widget.set("utilization", cpu_perc)
|
||||||
|
widget.set("theme.minwidth", self._format.format(100.0 - 10e-20))
|
||||||
|
|
||||||
core.input.register(
|
core.input.register(
|
||||||
self, button=core.input.LEFT_MOUSE, cmd="gnome-system-monitor"
|
self, button=core.input.LEFT_MOUSE, cmd="gnome-system-monitor"
|
||||||
)
|
)
|
||||||
|
@ -34,14 +42,19 @@ class Module(core.module.Module):
|
||||||
def _format(self):
|
def _format(self):
|
||||||
return self.parameter("format", "{:.01f}%")
|
return self.parameter("format", "{:.01f}%")
|
||||||
|
|
||||||
def utilization(self, _):
|
def utilization(self, widget):
|
||||||
return self._format.format(self._utilization)
|
return self._format.format(widget.get("utilization", 0.0))
|
||||||
|
|
||||||
|
def cpu_utilization(self):
|
||||||
|
tmp = psutil.cpu_percent(percpu=self._percpu)
|
||||||
|
return tmp if self._percpu else [tmp]
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
self._utilization = psutil.cpu_percent(percpu=False)
|
for idx, cpu_perc in enumerate(self.cpu_utilization()):
|
||||||
|
self.widgets()[idx].set("utilization", cpu_perc)
|
||||||
|
|
||||||
def state(self, _):
|
def state(self, widget):
|
||||||
return self.threshold_state(self._utilization, 70, 80)
|
return self.threshold_state(widget.get("utilization", 0.0), 70, 80)
|
||||||
|
|
||||||
|
|
||||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
||||||
|
|
|
@ -21,7 +21,6 @@ class Module(core.module.Module):
|
||||||
super().__init__(config, theme, core.widget.Widget(self.full_text))
|
super().__init__(config, theme, core.widget.Widget(self.full_text))
|
||||||
|
|
||||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd="calendar")
|
core.input.register(self, button=core.input.LEFT_MOUSE, cmd="calendar")
|
||||||
self._fmt = self.parameter("format", self.default_format())
|
|
||||||
l = locale.getdefaultlocale()
|
l = locale.getdefaultlocale()
|
||||||
if not l or l == (None, None):
|
if not l or l == (None, None):
|
||||||
l = ("en_US", "UTF-8")
|
l = ("en_US", "UTF-8")
|
||||||
|
@ -36,7 +35,8 @@ class Module(core.module.Module):
|
||||||
|
|
||||||
def full_text(self, widget):
|
def full_text(self, widget):
|
||||||
enc = locale.getpreferredencoding()
|
enc = locale.getpreferredencoding()
|
||||||
retval = datetime.datetime.now().strftime(self._fmt)
|
fmt = self.parameter("format", self.default_format())
|
||||||
|
retval = datetime.datetime.now().strftime(fmt)
|
||||||
if hasattr(retval, "decode"):
|
if hasattr(retval, "decode"):
|
||||||
return retval.decode(enc)
|
return retval.decode(enc)
|
||||||
return retval
|
return retval
|
||||||
|
|
|
@ -8,6 +8,7 @@ Parameters:
|
||||||
* disk.path: Path to calculate disk usage from (defaults to /)
|
* disk.path: Path to calculate disk usage from (defaults to /)
|
||||||
* disk.open: Which application / file manager to launch (default xdg-open)
|
* disk.open: Which application / file manager to launch (default xdg-open)
|
||||||
* disk.format: Format string, tags {path}, {used}, {left}, {size} and {percent} (defaults to '{path} {used}/{size} ({percent:05.02f}%)')
|
* disk.format: Format string, tags {path}, {used}, {left}, {size} and {percent} (defaults to '{path} {used}/{size} ({percent:05.02f}%)')
|
||||||
|
* disk.system: Unit system to use - SI (KB, MB, ...) or IEC (KiB, MiB, ...) (defaults to 'IEC')
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
@ -25,6 +26,7 @@ class Module(core.module.Module):
|
||||||
|
|
||||||
self._path = self.parameter("path", "/")
|
self._path = self.parameter("path", "/")
|
||||||
self._format = self.parameter("format", "{used}/{size} ({percent:05.02f}%)")
|
self._format = self.parameter("format", "{used}/{size} ({percent:05.02f}%)")
|
||||||
|
self._system = self.parameter("system", "IEC")
|
||||||
|
|
||||||
self._used = 0
|
self._used = 0
|
||||||
self._left = 0
|
self._left = 0
|
||||||
|
@ -38,9 +40,9 @@ class Module(core.module.Module):
|
||||||
)
|
)
|
||||||
|
|
||||||
def diskspace(self, widget):
|
def diskspace(self, widget):
|
||||||
used_str = util.format.byte(self._used)
|
used_str = util.format.byte(self._used, sys=self._system)
|
||||||
size_str = util.format.byte(self._size)
|
size_str = util.format.byte(self._size, sys=self._system)
|
||||||
left_str = util.format.byte(self._left)
|
left_str = util.format.byte(self._left, sys=self._system)
|
||||||
percent_str = self._percent
|
percent_str = self._percent
|
||||||
|
|
||||||
return self._format.format(
|
return self._format.format(
|
||||||
|
|
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()
|
self._cpus = multiprocessing.cpu_count()
|
||||||
except NotImplementedError as e:
|
except NotImplementedError as e:
|
||||||
self._cpus = 1
|
self._cpus = 1
|
||||||
|
|
||||||
core.input.register(
|
core.input.register(
|
||||||
self, button=core.input.LEFT_MOUSE, cmd="gnome-system-monitor"
|
self, button=core.input.LEFT_MOUSE, cmd="gnome-system-monitor"
|
||||||
)
|
)
|
||||||
|
|
|
@ -41,18 +41,8 @@ class Module(core.module.Module):
|
||||||
return self._format.format(**self._mem)
|
return self._format.format(**self._mem)
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
data = {}
|
data = self.__parse_meminfo()
|
||||||
with open("/proc/meminfo", "r") as f:
|
|
||||||
for line in f:
|
|
||||||
tmp = re.split(r"[:\s]+", line)
|
|
||||||
value = int(tmp[1])
|
|
||||||
if tmp[2] == "kB":
|
|
||||||
value = value * 1024
|
|
||||||
if tmp[2] == "mB":
|
|
||||||
value = value * 1024 * 1024
|
|
||||||
if tmp[2] == "gB":
|
|
||||||
value = value * 1024 * 1024 * 1024
|
|
||||||
data[tmp[0]] = value
|
|
||||||
if "MemAvailable" in data:
|
if "MemAvailable" in data:
|
||||||
used = data["MemTotal"] - data["MemAvailable"]
|
used = data["MemTotal"] - data["MemAvailable"]
|
||||||
else:
|
else:
|
||||||
|
@ -78,5 +68,28 @@ class Module(core.module.Module):
|
||||||
return "warning"
|
return "warning"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def __parse_meminfo(self):
|
||||||
|
data = {}
|
||||||
|
with open("/proc/meminfo", "r") as f:
|
||||||
|
# https://bugs.python.org/issue32933
|
||||||
|
for line in f.readlines():
|
||||||
|
tmp = re.split(r"[:\s]+", line)
|
||||||
|
value = self.__parse_value(tmp)
|
||||||
|
|
||||||
|
data[tmp[0]] = value
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def __parse_value(self, data):
|
||||||
|
value = int(data[1])
|
||||||
|
|
||||||
|
if data[2] == "kB":
|
||||||
|
value = value * 1024
|
||||||
|
if data[2] == "mB":
|
||||||
|
value = value * 1024 * 1024
|
||||||
|
if data[2] == "gB":
|
||||||
|
value = value * 1024 * 1024 * 1024
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
||||||
|
|
|
@ -7,12 +7,15 @@ Requires the following python module:
|
||||||
|
|
||||||
Requires the following executable:
|
Requires the following executable:
|
||||||
* iw
|
* iw
|
||||||
|
* (until and including 2.0.5: iwgetid)
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
* nic.exclude: Comma-separated list of interface prefixes to exclude (defaults to 'lo,virbr,docker,vboxnet,veth,br')
|
* nic.exclude: Comma-separated list of interface prefixes (supporting regular expressions) to exclude (defaults to 'lo,virbr,docker,vboxnet,veth,br,.*:avahi')
|
||||||
* nic.include: Comma-separated list of interfaces to include
|
* nic.include: Comma-separated list of interfaces to include
|
||||||
* nic.states: Comma-separated list of states to show (prefix with '^' to invert - i.e. ^down -> show all devices that are not in state down)
|
* nic.states: Comma-separated list of states to show (prefix with '^' to invert - i.e. ^down -> show all devices that are not in state down)
|
||||||
* nic.format: Format string (defaults to '{intf} {state} {ip} {ssid}')
|
* nic.format: Format string (defaults to '{intf} {state} {ip} {ssid} {strength}')
|
||||||
|
* nic.strength_warning: Integer to set the threshold for warning state (defaults to 50)
|
||||||
|
* nic.strength_critical: Integer to set the threshold for critical state (defaults to 30)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
@ -27,17 +30,14 @@ import util.format
|
||||||
|
|
||||||
|
|
||||||
class Module(core.module.Module):
|
class Module(core.module.Module):
|
||||||
@core.decorators.every(seconds=10)
|
@core.decorators.every(seconds=5)
|
||||||
def __init__(self, config, theme):
|
def __init__(self, config, theme):
|
||||||
widgets = []
|
widgets = []
|
||||||
super().__init__(config, theme, widgets)
|
super().__init__(config, theme, widgets)
|
||||||
self._exclude = tuple(
|
self._exclude = util.format.aslist(
|
||||||
filter(
|
self.parameter("exclude", "lo,virbr,docker,vboxnet,veth,br,.*:avahi")
|
||||||
len,
|
|
||||||
self.parameter("exclude", "lo,virbr,docker,vboxnet,veth,br").split(","),
|
|
||||||
)
|
)
|
||||||
)
|
self._include = util.format.aslist(self.parameter("include", ""))
|
||||||
self._include = self.parameter("include", "").split(",")
|
|
||||||
|
|
||||||
self._states = {"include": [], "exclude": []}
|
self._states = {"include": [], "exclude": []}
|
||||||
for state in tuple(
|
for state in tuple(
|
||||||
|
@ -47,7 +47,15 @@ class Module(core.module.Module):
|
||||||
self._states["exclude"].append(state[1:])
|
self._states["exclude"].append(state[1:])
|
||||||
else:
|
else:
|
||||||
self._states["include"].append(state)
|
self._states["include"].append(state)
|
||||||
self._format = self.parameter("format", "{intf} {state} {ip} {ssid}")
|
self._format = self.parameter("format", "{intf} {state} {ip} {ssid} {strength}")
|
||||||
|
|
||||||
|
self._strength_threshold_critical = self.parameter("strength_critical", 30)
|
||||||
|
self._strength_threshold_warning = self.parameter("strength_warning", 50)
|
||||||
|
|
||||||
|
# Limits for the accepted dBm values of wifi strength
|
||||||
|
self.__strength_dbm_lower_bound = -110
|
||||||
|
self.__strength_dbm_upper_bound = -30
|
||||||
|
|
||||||
self.iw = shutil.which("iw")
|
self.iw = shutil.which("iw")
|
||||||
self._update_widgets(widgets)
|
self._update_widgets(widgets)
|
||||||
|
|
||||||
|
@ -66,6 +74,14 @@ class Module(core.module.Module):
|
||||||
iftype = "wireless" if self._iswlan(intf) else "wired"
|
iftype = "wireless" if self._iswlan(intf) else "wired"
|
||||||
iftype = "tunnel" if self._istunnel(intf) else iftype
|
iftype = "tunnel" if self._istunnel(intf) else iftype
|
||||||
|
|
||||||
|
# "strength" is none if interface type is not wlan
|
||||||
|
strength = widget.get("strength")
|
||||||
|
if self._iswlan(intf) and strength:
|
||||||
|
if strength < self._strength_threshold_critical:
|
||||||
|
states.append("critical")
|
||||||
|
elif strength < self._strength_threshold_warning:
|
||||||
|
states.append("warning")
|
||||||
|
|
||||||
states.append("{}-{}".format(iftype, widget.get("state")))
|
states.append("{}-{}".format(iftype, widget.get("state")))
|
||||||
|
|
||||||
return states
|
return states
|
||||||
|
@ -89,11 +105,18 @@ class Module(core.module.Module):
|
||||||
return []
|
return []
|
||||||
return retval
|
return retval
|
||||||
|
|
||||||
|
def _excluded(self, intf):
|
||||||
|
for e in self._exclude:
|
||||||
|
if re.match(e, intf):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def _update_widgets(self, widgets):
|
def _update_widgets(self, widgets):
|
||||||
self.clear_widgets()
|
self.clear_widgets()
|
||||||
interfaces = [
|
interfaces = []
|
||||||
i for i in netifaces.interfaces() if not i.startswith(self._exclude)
|
for i in netifaces.interfaces():
|
||||||
]
|
if not self._excluded(i):
|
||||||
|
interfaces.append(i)
|
||||||
interfaces.extend([i for i in netifaces.interfaces() if i in self._include])
|
interfaces.extend([i for i in netifaces.interfaces() if i in self._include])
|
||||||
|
|
||||||
for intf in interfaces:
|
for intf in interfaces:
|
||||||
|
@ -111,6 +134,9 @@ class Module(core.module.Module):
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
strength_dbm = self.get_strength_dbm(intf)
|
||||||
|
strength_percent = self.convert_strength_dbm_percent(strength_dbm)
|
||||||
|
|
||||||
widget = self.widget(intf)
|
widget = self.widget(intf)
|
||||||
if not widget:
|
if not widget:
|
||||||
widget = self.add_widget(name=intf)
|
widget = self.add_widget(name=intf)
|
||||||
|
@ -121,22 +147,44 @@ class Module(core.module.Module):
|
||||||
ip=", ".join(addr),
|
ip=", ".join(addr),
|
||||||
intf=intf,
|
intf=intf,
|
||||||
state=state,
|
state=state,
|
||||||
|
strength=str(strength_percent) + "%" if strength_percent else "",
|
||||||
ssid=self.get_ssid(intf),
|
ssid=self.get_ssid(intf),
|
||||||
).split()
|
).split()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
widget.set("intf", intf)
|
widget.set("intf", intf)
|
||||||
widget.set("state", state)
|
widget.set("state", state)
|
||||||
|
widget.set("strength", strength_percent)
|
||||||
|
|
||||||
def get_ssid(self, intf):
|
def get_ssid(self, intf):
|
||||||
if self._iswlan(intf) and not self._istunnel(intf) and self.iw:
|
if not self._iswlan(intf) or self._istunnel(intf) or not self.iw:
|
||||||
ssid = util.cli.execute("{} dev {} link".format(self.iw, intf))
|
|
||||||
found_ssid = re.findall("SSID:\s(.+)", ssid)
|
|
||||||
if len(found_ssid) > 0:
|
|
||||||
return found_ssid[0]
|
|
||||||
else:
|
|
||||||
return ""
|
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 ""
|
return ""
|
||||||
|
|
||||||
|
def get_strength_dbm(self, intf):
|
||||||
|
if not self._iswlan(intf) or self._istunnel(intf) or not self.iw:
|
||||||
|
return None
|
||||||
|
|
||||||
|
with open("/proc/net/wireless", "r") as file:
|
||||||
|
for line in file:
|
||||||
|
if intf in line:
|
||||||
|
# Remove trailing . by slicing it off ;)
|
||||||
|
strength_dbm = line.split()[3][:-1]
|
||||||
|
return util.format.asint(strength_dbm,
|
||||||
|
minimum=self.__strength_dbm_lower_bound,
|
||||||
|
maximum=self.__strength_dbm_upper_bound)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def convert_strength_dbm_percent(self, signal):
|
||||||
|
return int(100 * ((signal + 100) / 70.0)) if signal else None
|
||||||
|
|
||||||
|
|
||||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
||||||
|
|
|
@ -54,7 +54,7 @@ def get_redshift_value(module):
|
||||||
for line in res.split("\n"):
|
for line in res.split("\n"):
|
||||||
line = line.lower()
|
line = line.lower()
|
||||||
if "temperature" in line:
|
if "temperature" in line:
|
||||||
widget.set("temp", line.split(" ")[2])
|
widget.set("temp", line.split(" ")[2].upper())
|
||||||
if "period" in line:
|
if "period" in line:
|
||||||
state = line.split(" ")[1]
|
state = line.split(" ")[1]
|
||||||
if "day" in state:
|
if "day" in state:
|
||||||
|
@ -101,7 +101,7 @@ class Module(core.module.Module):
|
||||||
return val
|
return val
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
if self.__thread is not None and self.__thread.isAlive():
|
if self.__thread is not None and self.__thread.is_alive():
|
||||||
return
|
return
|
||||||
self.__thread = threading.Thread(target=get_redshift_value, args=(self,))
|
self.__thread = threading.Thread(target=get_redshift_value, args=(self,))
|
||||||
self.__thread.start()
|
self.__thread.start()
|
||||||
|
|
|
@ -9,7 +9,7 @@ Parameters:
|
||||||
import core.module
|
import core.module
|
||||||
import core.widget
|
import core.widget
|
||||||
import core.decorators
|
import core.decorators
|
||||||
|
import core.input
|
||||||
|
|
||||||
class Module(core.module.Module):
|
class Module(core.module.Module):
|
||||||
@core.decorators.every(minutes=60)
|
@core.decorators.every(minutes=60)
|
||||||
|
@ -20,5 +20,8 @@ class Module(core.module.Module):
|
||||||
def text(self, _):
|
def text(self, _):
|
||||||
return self.__text
|
return self.__text
|
||||||
|
|
||||||
|
def update_text(self, event):
|
||||||
|
self.__text = core.input.button_name(event["button"])
|
||||||
|
|
||||||
|
|
||||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
||||||
|
|
|
@ -12,7 +12,6 @@ from .datetime import Module
|
||||||
|
|
||||||
|
|
||||||
class Module(Module):
|
class Module(Module):
|
||||||
@core.decorators.every(seconds=59) # ensures one update per minute
|
|
||||||
def __init__(self, config, theme):
|
def __init__(self, config, theme):
|
||||||
super().__init__(config, theme)
|
super().__init__(config, theme)
|
||||||
|
|
||||||
|
|
|
@ -71,22 +71,33 @@ def astemperature(val, unit="metric"):
|
||||||
return "{}°{}".format(int(val), __UNITS.get(unit.lower(), __UNITS["default"]))
|
return "{}°{}".format(int(val), __UNITS.get(unit.lower(), __UNITS["default"]))
|
||||||
|
|
||||||
|
|
||||||
def byte(val, fmt="{:.2f}"):
|
def byte(val, fmt="{:.2f}", sys="IEC"):
|
||||||
"""Returns a byte representation of the input value
|
"""Returns a byte representation of the input value
|
||||||
|
|
||||||
:param val: value to format, must be convertible into a float
|
:param val: value to format, must be convertible into a float
|
||||||
:param fmt: optional output format string, defaults to {:.2f}
|
:param fmt: optional output format string, defaults to {:.2f}
|
||||||
|
:param sys: optional unit system specifier - SI (kilo, Mega, Giga, ...) or
|
||||||
|
IEC (kibi, Mebi, Gibi, ...) - defaults to IEC
|
||||||
|
|
||||||
:return: byte representation (e.g. <X> KiB, GiB, etc.) of the input value
|
:return: byte representation (e.g. <X> KiB, GiB, etc.) of the input value
|
||||||
:rtype: string
|
:rtype: string
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if sys == "IEC":
|
||||||
|
div = 1024.0
|
||||||
|
units = ["", "Ki", "Mi", "Gi", "Ti"]
|
||||||
|
final = "TiB"
|
||||||
|
elif sys == "SI":
|
||||||
|
div = 1000.0
|
||||||
|
units = ["", "K", "M", "G", "T"]
|
||||||
|
final = "TB"
|
||||||
|
|
||||||
val = float(val)
|
val = float(val)
|
||||||
for unit in ["", "Ki", "Mi", "Gi"]:
|
for unit in units:
|
||||||
if val < 1024.0:
|
if val < div:
|
||||||
return "{}{}B".format(fmt, unit).format(val)
|
return "{}{}B".format(fmt, unit).format(val)
|
||||||
val /= 1024.0
|
val /= div
|
||||||
return "{}GiB".format(fmt).format(val * 1024.0)
|
return "{}{}".format(fmt).format(val * div, final)
|
||||||
|
|
||||||
|
|
||||||
__seconds_pattern = re.compile(r"(([\d\.?]+)h)?(([\d\.]+)m)?([\d\.]+)?s?")
|
__seconds_pattern = re.compile(r"(([\d\.?]+)h)?(([\d\.]+)m)?([\d\.]+)?s?")
|
||||||
|
|
|
@ -59,11 +59,11 @@ def __load():
|
||||||
__next = time.time() + 60 * 30 # error - try again every 30m
|
__next = time.time() + 60 * 30 # error - try again every 30m
|
||||||
|
|
||||||
|
|
||||||
def __get(name, default=None):
|
def __get(name):
|
||||||
global __data
|
global __data
|
||||||
if not __data or __expired():
|
if not __data or __expired():
|
||||||
__load()
|
__load()
|
||||||
return __data.get(name, default)
|
return __data[name]
|
||||||
|
|
||||||
|
|
||||||
def reset():
|
def reset():
|
||||||
|
|
|
@ -49,6 +49,7 @@ class menu(object):
|
||||||
return self._menu
|
return self._menu
|
||||||
|
|
||||||
def __on_focus_out(self, event=None):
|
def __on_focus_out(self, event=None):
|
||||||
|
self.running = False
|
||||||
self._root.destroy()
|
self._root.destroy()
|
||||||
|
|
||||||
def __on_click(self, callback):
|
def __on_click(self, callback):
|
||||||
|
|
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
|
General guidelines
|
||||||
==================
|
==================
|
||||||
|
|
||||||
Writing unit tests
|
Not much, right now. If you have an idea and some code, just
|
||||||
------------------
|
create a PR, I will gladly review and comment (and, likely, merge)
|
||||||
|
|
||||||
Some general hints:
|
Just one minor note: ``bumblebee-status`` is mostly a one-person,
|
||||||
|
spare-time project, so please be patient when answering an issue,
|
||||||
|
question or PR takes a while.
|
||||||
|
|
||||||
- Tests should run with just Python Standard Library modules installed
|
Also, the (small) community that has gathered around ``bumblebee-status``
|
||||||
(i.e. if there are additional requirements, the test should be skipped
|
is extremely friendly and helpful, so don't hesitate to create issues
|
||||||
if those are missing)
|
with questions, somebody will always come up with a useful answer.
|
||||||
- Tests should run even if there is no network connectivity (please mock
|
|
||||||
urllib calls, for example)
|
|
||||||
- Tests should be stable and not require modifications every time the
|
|
||||||
tested code's implementation changes slightly (been there / done that)
|
|
||||||
|
|
||||||
Right now, ``bumblebee-status`` is moving away from Python's
|
:)
|
||||||
built-in ``unittest`` framework (tests located inside ``tests/``)
|
|
||||||
and towards ``pytest`` (tests located inside ``pytests/``).
|
|
||||||
|
|
||||||
First implication: To run the new tests, you need to have ``pytest``
|
|
||||||
installed, it is not part of the Python Standard Library. Most
|
|
||||||
distributions call the package ``python-pytest`` or ``python3-pytest``
|
|
||||||
or something similar (or you just use ``pip install --use pytest``)
|
|
||||||
|
|
||||||
Aside from that, you just write your tests using ``pytest`` as usual,
|
|
||||||
with one big caveat:
|
|
||||||
|
|
||||||
**If** you create a new directory inside ``pytests/``, you need to
|
|
||||||
also create a file called ``__init__.py`` inside that, otherwise,
|
|
||||||
modules won't load correctly.
|
|
||||||
|
|
||||||
For examples, just browse the existing code. A good, minimal sample
|
|
||||||
for unit testing ``bumblebee-status`` is ``pytests/core/test_event.py``.
|
|
||||||
|
|
|
@ -8,4 +8,5 @@ Developer's Guide
|
||||||
general
|
general
|
||||||
module
|
module
|
||||||
theme
|
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`` (i.e. a module called
|
||||||
``bumblebee_status/modules/contrib/test.py`` will be loaded using
|
``bumblebee_status/modules/contrib/test.py`` will be loaded using
|
||||||
``bumblebee-status -m test``)
|
``bumblebee-status -m test``)
|
||||||
|
- Alternatively, you can put your module in ``~/.config/bumblebee-status/modules/``
|
||||||
|
- The module name must follow the `Python Naming Conventions <https://www.python.org/dev/peps/pep-0008/#package-and-module-names>`_
|
||||||
- See below for how to actually write the module
|
- See below for how to actually write the module
|
||||||
- Test (run ``bumblebee-status`` in the CLI)
|
- Test (run ``bumblebee-status`` in the CLI)
|
||||||
- Make sure your changes don’t break anything: ``./coverage.sh``
|
- 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
|
``black -t py34`` before submitting
|
||||||
|
|
||||||
Pull requests
|
Pull requests
|
||||||
|
@ -22,7 +24,7 @@ Pull requests
|
||||||
|
|
||||||
The project **gladly** accepts PRs for bugfixes, new functionality, new
|
The project **gladly** accepts PRs for bugfixes, new functionality, new
|
||||||
modules, etc. When you feel comfortable with what you’ve developed,
|
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
|
Coding guidelines
|
||||||
-----------------
|
-----------------
|
||||||
|
@ -65,7 +67,7 @@ Of modules and widgets
|
||||||
|
|
||||||
There are two important concepts for module writers: - A module is
|
There are two important concepts for module writers: - A module is
|
||||||
something that offers a single set of coherent functionality - A module
|
something that offers a single set of coherent functionality - A module
|
||||||
has 1 to n “widgets”, which translates to individual blocks in the i3bar
|
has 1 to n “widgets”, which translates to individual blocks in the i3bar.
|
||||||
|
|
||||||
Very often, this is a 1:1 relationship, and a single module has a single
|
Very often, this is a 1:1 relationship, and a single module has a single
|
||||||
widget. If that’s the case for you, you can stop reading now :)
|
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
|
Advanced usage
|
||||||
===========================
|
===========================
|
||||||
|
|
||||||
|
Intervals
|
||||||
|
---------
|
||||||
|
|
||||||
|
Some modules define their own update intervals (e.g. most modules that query
|
||||||
|
an online service), such as to not cause a storm of "once every second" queries.
|
||||||
|
|
||||||
|
For such modules, the "global" interval defined via the ``interval`` parameter effectively defines the
|
||||||
|
highest possible "resolution". If you have a global interval of 10s, for example,
|
||||||
|
any other module can update at 10s, 20s, 30s, etc., but not every 25s. The status
|
||||||
|
bar will internally always align to the next future time slot.
|
||||||
|
|
||||||
|
The update interval can also be changed on a per-module basis, like
|
||||||
|
this (overriding the default module interval indicated above):
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ ./bumblebee-status -m cpu memory -p cpu.interval=5s memory.interval=1m
|
||||||
|
|
||||||
|
|
||||||
Events
|
Events
|
||||||
------
|
------
|
||||||
|
|
||||||
|
@ -87,6 +106,19 @@ attention, it will remain hidden. Note that this parameter is specified
|
||||||
*in addition* to ``-m`` (i.e. to autohide the CPU module, you would use
|
*in addition* to ``-m`` (i.e. to autohide the CPU module, you would use
|
||||||
``bumblebee-status -m cpu memory traffic -a cpu``).
|
``bumblebee-status -m cpu memory traffic -a cpu``).
|
||||||
|
|
||||||
|
Scrolling widget text
|
||||||
|
-----------------------
|
||||||
|
Some widgets support scrolling for long text (e.g. most music player
|
||||||
|
widgets, rss, etc.). Those have some additional settings for customizing
|
||||||
|
the scrolling behaviour, in particular:
|
||||||
|
|
||||||
|
- ``scrolling.width``: Desired width of the scrolling panel
|
||||||
|
- ``scrolling.makewide``: If set to true, extends texts shorter than
|
||||||
|
``scrolling.width`` to that width
|
||||||
|
- ``scrolling.bounce``: If set to true, bounces the text when it reaches
|
||||||
|
the end, otherwise, it behaves like marquee (scroll-through) text
|
||||||
|
- ``scrolling.speed``: Defines the scroll speed, in characters per update
|
||||||
|
|
||||||
Additional widget theme settings
|
Additional widget theme settings
|
||||||
--------------------------------
|
--------------------------------
|
||||||
|
|
||||||
|
@ -128,6 +160,7 @@ Configuration files have the following format:
|
||||||
|
|
||||||
[core]
|
[core]
|
||||||
modules = <comma-separated list of modules to load>
|
modules = <comma-separated list of modules to load>
|
||||||
|
autohide = <comma-separated list of modules to hide, unless in warning/error state>
|
||||||
theme = <theme to use by default>
|
theme = <theme to use by default>
|
||||||
|
|
||||||
[module-parameters]
|
[module-parameters]
|
||||||
|
|
|
@ -20,15 +20,15 @@ feature requests, etc. :)
|
||||||
|
|
||||||
Thanks a lot!
|
Thanks a lot!
|
||||||
|
|
||||||
+------------------------------------+-------------------------+
|
+------------------------------------+------------------------------+
|
||||||
| **Required i3wm version** | 4.12+ |
|
| **Required i3wm version** | 4.12+ |
|
||||||
+------------------------------------+-------------------------+
|
+------------------------------------+------------------------------+
|
||||||
| **Supported Python versions** | 3.4, 3.5, 3.6, 3.7, 3.8 |
|
| **Supported Python versions** | 3.4, 3.5, 3.6, 3.7, 3.8, 3.9 |
|
||||||
+------------------------------------+-------------------------+
|
+------------------------------------+------------------------------+
|
||||||
| **Supported FontAwesome versions** | 4 only |
|
| **Supported FontAwesome versions** | 4 only |
|
||||||
+------------------------------------+-------------------------+
|
+------------------------------------+------------------------------+
|
||||||
| **Per-module requirements** | see :doc:`modules` |
|
| **Per-module requirements** | see :doc:`modules` |
|
||||||
+------------------------------------+-------------------------+
|
+------------------------------------+------------------------------+
|
||||||
|
|
||||||
see :doc:`FAQ` for details on this
|
see :doc:`FAQ` for details on this
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,9 @@ Installation
|
||||||
# will install bumblebee-status into ~/.local/bin/bumblebee-status
|
# will install bumblebee-status into ~/.local/bin/bumblebee-status
|
||||||
pip install --user bumblebee-status
|
pip install --user bumblebee-status
|
||||||
|
|
||||||
|
|
||||||
|
There is also a SlackBuild available here: [slackbuilds:bumblebee-status](http://slackbuilds.org/repository/14.2/desktop/bumblebee-status/) - many thanks to [@Tonus1](https://github.com/Tonus1)!
|
||||||
|
|
||||||
Dependencies
|
Dependencies
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
@ -56,12 +59,15 @@ To change the update interval, use:
|
||||||
|
|
||||||
$ ./bumblebee-status -m <list of modules> -p interval=<interval in seconds>
|
$ ./bumblebee-status -m <list of modules> -p interval=<interval in seconds>
|
||||||
|
|
||||||
The update interval can also be changed on a per-module basis, like
|
The update interval is the global "refresh" interval of the modules (i.e. how often
|
||||||
this:
|
the bar will be updated with new data). The default interval is one second. It is
|
||||||
|
possible to use suffixes such as "m" (for minutes), or "h" for hours (e.g.
|
||||||
|
``-p interval=5m`` to update once every 5 minutes.
|
||||||
|
|
||||||
.. code-block:: bash
|
Note that some modules define their own intervals (e.g. most modules that query
|
||||||
|
an online service), such as to not cause a storm of "once every second" queries.
|
||||||
|
|
||||||
$ ./bumblebee-status -m cpu memory -p cpu.interval=5s memory.interval=1m
|
For more details on that, please refer to :doc:`features`.
|
||||||
|
|
||||||
All modules can be given “aliases” using ``<module name>:<alias>``, by
|
All modules can be given “aliases” using ``<module name>:<alias>``, by
|
||||||
which they can be parametrized, for example:
|
which they can be parametrized, for example:
|
||||||
|
|
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.warning : Warning threshold in % of CPU usage (defaults to 70%)
|
||||||
* cpu.critical: Critical threshold in % of CPU usage (defaults to 80%)
|
* cpu.critical: Critical threshold in % of CPU usage (defaults to 80%)
|
||||||
* cpu.format : Format string (defaults to '{:.01f}%')
|
* cpu.format : Format string (defaults to '{:.01f}%')
|
||||||
|
* cpu.percpu : If set to true, show each individual cpu (defaults to false)
|
||||||
|
|
||||||
.. image:: ../screenshots/cpu.png
|
.. image:: ../screenshots/cpu.png
|
||||||
|
|
||||||
|
@ -63,6 +64,7 @@ Parameters:
|
||||||
* disk.path: Path to calculate disk usage from (defaults to /)
|
* disk.path: Path to calculate disk usage from (defaults to /)
|
||||||
* disk.open: Which application / file manager to launch (default xdg-open)
|
* disk.open: Which application / file manager to launch (default xdg-open)
|
||||||
* disk.format: Format string, tags {path}, {used}, {left}, {size} and {percent} (defaults to '{path} {used}/{size} ({percent:05.02f}%)')
|
* disk.format: Format string, tags {path}, {used}, {left}, {size} and {percent} (defaults to '{path} {used}/{size} ({percent:05.02f}%)')
|
||||||
|
* disk.system: Unit system to use - SI (KB, MB, ...) or IEC (KiB, MiB, ...) (defaults to 'IEC')
|
||||||
|
|
||||||
.. image:: ../screenshots/disk.png
|
.. image:: ../screenshots/disk.png
|
||||||
|
|
||||||
|
@ -83,11 +85,33 @@ Requires:
|
||||||
|
|
||||||
.. image:: ../screenshots/git.png
|
.. image:: ../screenshots/git.png
|
||||||
|
|
||||||
|
keys
|
||||||
|
~~~~
|
||||||
|
|
||||||
|
Shows when a key is pressed
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
* keys.keys: Comma-separated list of keys to monitor (defaults to "")
|
||||||
|
|
||||||
layout-xkb
|
layout-xkb
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
|
|
||||||
Displays the current keyboard layout using libX11
|
Displays the current keyboard layout using libX11
|
||||||
|
|
||||||
|
Requires the following library:
|
||||||
|
* libX11.so.6
|
||||||
|
and python module:
|
||||||
|
* xkbgroup
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
* layout-xkb.showname: Boolean that indicate whether the full name should be displayed. Defaults to false (only the symbol will be displayed)
|
||||||
|
* layout-xkb.show_variant: Boolean that indecates whether the variant name should be displayed. Defaults to true.
|
||||||
|
|
||||||
|
layout_xkb
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
Displays the current keyboard layout using libX11
|
||||||
|
|
||||||
Requires the following library:
|
Requires the following library:
|
||||||
* libX11.so.6
|
* libX11.so.6
|
||||||
and python module:
|
and python module:
|
||||||
|
@ -141,9 +165,10 @@ Requires the following python module:
|
||||||
|
|
||||||
Requires the following executable:
|
Requires the following executable:
|
||||||
* iw
|
* iw
|
||||||
|
* (until and including 2.0.5: iwgetid)
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
* nic.exclude: Comma-separated list of interface prefixes to exclude (defaults to 'lo,virbr,docker,vboxnet,veth,br')
|
* nic.exclude: Comma-separated list of interface prefixes (supporting regular expressions) to exclude (defaults to 'lo,virbr,docker,vboxnet,veth,br,.*:avahi')
|
||||||
* nic.include: Comma-separated list of interfaces to include
|
* nic.include: Comma-separated list of interfaces to include
|
||||||
* nic.states: Comma-separated list of states to show (prefix with '^' to invert - i.e. ^down -> show all devices that are not in state down)
|
* nic.states: Comma-separated list of states to show (prefix with '^' to invert - i.e. ^down -> show all devices that are not in state down)
|
||||||
* nic.format: Format string (defaults to '{intf} {state} {ip} {ssid}')
|
* nic.format: Format string (defaults to '{intf} {state} {ip} {ssid}')
|
||||||
|
@ -278,6 +303,7 @@ Parameters:
|
||||||
* vault.location: Location of the password store (defaults to ~/.password-store)
|
* vault.location: Location of the password store (defaults to ~/.password-store)
|
||||||
* vault.offx: x-axis offset of popup menu (defaults to 0)
|
* vault.offx: x-axis offset of popup menu (defaults to 0)
|
||||||
* vault.offy: y-axis offset of popup menu (defaults to 0)
|
* vault.offy: y-axis offset of popup menu (defaults to 0)
|
||||||
|
* vault.text: Text to display on the widget (defaults to <click-for-password>)
|
||||||
|
|
||||||
Many thanks to `bbernhard <https://github.com/bbernhard>`_ for the idea!
|
Many thanks to `bbernhard <https://github.com/bbernhard>`_ for the idea!
|
||||||
|
|
||||||
|
@ -294,6 +320,9 @@ Parameters:
|
||||||
and appending a file '~/.config/i3/config.<screen name>' for every screen.
|
and appending a file '~/.config/i3/config.<screen name>' for every screen.
|
||||||
* xrandr.autoupdate: If set to 'false', does *not* invoke xrandr automatically. Instead, the
|
* xrandr.autoupdate: If set to 'false', does *not* invoke xrandr automatically. Instead, the
|
||||||
module will only refresh when displays are enabled or disabled (defaults to true)
|
module will only refresh when displays are enabled or disabled (defaults to true)
|
||||||
|
* xrandr.exclude: Comma-separated list of display name prefixes to exclude
|
||||||
|
* xrandr.autotoggle: Boolean flag to automatically enable new displays (defaults to false)
|
||||||
|
* xrandr.autotoggle_side: Which side to put autotoggled displays on ('right' or 'left', defaults to 'right')
|
||||||
|
|
||||||
Requires the following python module:
|
Requires the following python module:
|
||||||
* (optional) i3 - if present, the need for updating the widget list is auto-detected
|
* (optional) i3 - if present, the need for updating the widget list is auto-detected
|
||||||
|
@ -362,6 +391,16 @@ Requires the following executable:
|
||||||
|
|
||||||
contributed by `lucassouto <https://github.com/lucassouto>`_ - many thanks!
|
contributed by `lucassouto <https://github.com/lucassouto>`_ - many thanks!
|
||||||
|
|
||||||
|
arch_update
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
Check updates to Arch Linux.
|
||||||
|
|
||||||
|
Requires the following executable:
|
||||||
|
* checkupdates (from pacman-contrib)
|
||||||
|
|
||||||
|
contributed by `lucassouto <https://github.com/lucassouto>`_ - many thanks!
|
||||||
|
|
||||||
battery
|
battery
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|
||||||
|
@ -392,6 +431,18 @@ Parameters:
|
||||||
|
|
||||||
contributed by `martindoublem <https://github.com/martindoublem>`_ - many thanks!
|
contributed by `martindoublem <https://github.com/martindoublem>`_ - many thanks!
|
||||||
|
|
||||||
|
battery_upower
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Displays battery status, remaining percentage and charging information.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
* battery-upower.warning : Warning threshold in % of remaining charge (defaults to 20)
|
||||||
|
* battery-upower.critical : Critical threshold in % of remaining charge (defaults to 10)
|
||||||
|
* battery-upower.showremaining : If set to true (default) shows the remaining time until the batteries are completely discharged
|
||||||
|
|
||||||
|
contributed by `martindoublem <https://github.com/martindoublem>`_ - many thanks!
|
||||||
|
|
||||||
bluetooth
|
bluetooth
|
||||||
~~~~~~~~~
|
~~~~~~~~~
|
||||||
|
|
||||||
|
@ -579,8 +630,6 @@ some media control bindings.
|
||||||
Left click toggles pause, scroll up skips the current song, scroll
|
Left click toggles pause, scroll up skips the current song, scroll
|
||||||
down returns to the previous song.
|
down returns to the previous song.
|
||||||
|
|
||||||
Requires the following library:
|
|
||||||
* subprocess
|
|
||||||
Parameters:
|
Parameters:
|
||||||
* deadbeef.format: Format string (defaults to '{artist} - {title}')
|
* deadbeef.format: Format string (defaults to '{artist} - {title}')
|
||||||
Available values are: {artist}, {title}, {album}, {length},
|
Available values are: {artist}, {title}, {album}, {length},
|
||||||
|
@ -636,9 +685,6 @@ Displays DNF package update information (<security>/<bugfixes>/<enhancements>/<o
|
||||||
Requires the following executable:
|
Requires the following executable:
|
||||||
* dnf
|
* dnf
|
||||||
|
|
||||||
Parameters:
|
|
||||||
* dnf.interval: Time in minutes between two consecutive update checks (defaults to 30 minutes)
|
|
||||||
|
|
||||||
.. image:: ../screenshots/dnf.png
|
.. image:: ../screenshots/dnf.png
|
||||||
|
|
||||||
docker_ps
|
docker_ps
|
||||||
|
@ -660,6 +706,40 @@ contributed by `eknoes <https://github.com/eknoes>`_ - many thanks!
|
||||||
|
|
||||||
.. image:: ../screenshots/dunst.png
|
.. image:: ../screenshots/dunst.png
|
||||||
|
|
||||||
|
dunstctl
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
Toggle dunst notifications using dunstctl.
|
||||||
|
|
||||||
|
When notifications are paused using this module dunst doesn't get killed and
|
||||||
|
you'll keep getting notifications on the background that will be displayed when
|
||||||
|
unpausing. This is specially useful if you're using dunst's scripting
|
||||||
|
(https://wiki.archlinux.org/index.php/Dunst#Scripting), which requires dunst to
|
||||||
|
be running. Scripts will be executed when dunst gets unpaused.
|
||||||
|
|
||||||
|
Requires:
|
||||||
|
* dunst v1.5.0+
|
||||||
|
|
||||||
|
contributed by `cristianmiranda <https://github.com/cristianmiranda>`_ - many thanks!
|
||||||
|
contributed by `joachimmathes <https://github.com/joachimmathes>`_ - many thanks!
|
||||||
|
|
||||||
|
.. image:: ../screenshots/dunstctl.png
|
||||||
|
|
||||||
|
emerge_status
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Display information about the currently running emerge process.
|
||||||
|
|
||||||
|
Requires the following executable:
|
||||||
|
* emerge
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
* emerge_status.format: Format string (defaults to '{current}/{total} {action} {category}/{pkg}')
|
||||||
|
|
||||||
|
This code is based on `emerge_status module from p3status <https://github.com/ultrabug/py3status/blob/master/py3status/modules/emerge_status.py>`_ original created by `AnwariasEu <https://github.com/AnwariasEu>`_.
|
||||||
|
|
||||||
|
.. image:: ../screenshots/emerge_status.png
|
||||||
|
|
||||||
getcrypto
|
getcrypto
|
||||||
~~~~~~~~~
|
~~~~~~~~~
|
||||||
|
|
||||||
|
@ -715,7 +795,7 @@ contributed by `TheEdgeOfRage <https://github.com/TheEdgeOfRage>`_ - many thanks
|
||||||
hddtemp
|
hddtemp
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|
||||||
Fetch hard drive temeperature data from a hddtemp daemon
|
Fetch hard drive temperature data from a hddtemp daemon
|
||||||
that runs on localhost and default port (7634)
|
that runs on localhost and default port (7634)
|
||||||
|
|
||||||
contributed by `somospocos <https://github.com/somospocos>`_ - many thanks!
|
contributed by `somospocos <https://github.com/somospocos>`_ - many thanks!
|
||||||
|
@ -788,6 +868,16 @@ Requires the following executable:
|
||||||
|
|
||||||
contributed by `somospocos <https://github.com/somospocos>`_ - many thanks!
|
contributed by `somospocos <https://github.com/somospocos>`_ - many thanks!
|
||||||
|
|
||||||
|
layout_xkbswitch
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Displays and changes the current keyboard layout
|
||||||
|
|
||||||
|
Requires the following executable:
|
||||||
|
* xkb-switch
|
||||||
|
|
||||||
|
contributed by `somospocos <https://github.com/somospocos>`_ - many thanks!
|
||||||
|
|
||||||
libvirtvms
|
libvirtvms
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -984,6 +1074,16 @@ Displays information about the current song in vlc, audacious, bmp, xmms2, spoti
|
||||||
Requires the following executable:
|
Requires the following executable:
|
||||||
* playerctl
|
* playerctl
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
* playerctl.format: Format string (defaults to '{{artist}} - {{title}} {{duration(position)}}/{{duration(mpris:length)}}').
|
||||||
|
The format string is passed to 'playerctl -f' as an argument. Read `the README <https://github.com/altdesktop/playerctl#printing-properties-and-metadata>`_ for more information.
|
||||||
|
* playerctl.layout: Comma-separated list to change order of widgets (defaults to song, previous, pause, next)
|
||||||
|
Widget names are: playerctl.song, playerctl.prev, playerctl.pause, playerctl.next
|
||||||
|
* playerctl.args: The arguments added to playerctl.
|
||||||
|
You can check 'playerctl --help' or `its readme <https://github.com/altdesktop/playerctl#using-the-cli>`_. For example, it could be '-p vlc,%any'.
|
||||||
|
|
||||||
|
Parameters are inspired by the `spotify` module, many thanks to its developers!
|
||||||
|
|
||||||
contributed by `smitajit <https://github.com/smitajit>`_ - many thanks!
|
contributed by `smitajit <https://github.com/smitajit>`_ - many thanks!
|
||||||
|
|
||||||
.. image:: ../screenshots/playerctl.png
|
.. image:: ../screenshots/playerctl.png
|
||||||
|
@ -1070,6 +1170,17 @@ publicip
|
||||||
|
|
||||||
Displays public IP address
|
Displays public IP address
|
||||||
|
|
||||||
|
rofication
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
Rofication indicator
|
||||||
|
|
||||||
|
https://github.com/DaveDavenport/Rofication
|
||||||
|
simple module to show an icon + the number of notifications stored in rofication
|
||||||
|
module will have normal highlighting if there are zero notifications,
|
||||||
|
"warning" highlighting if there are nonzero notifications,
|
||||||
|
"critical" highlighting if there are any critical notifications
|
||||||
|
|
||||||
rotation
|
rotation
|
||||||
~~~~~~~~
|
~~~~~~~~
|
||||||
|
|
||||||
|
@ -1099,6 +1210,9 @@ sensors
|
||||||
Displays sensor temperature
|
Displays sensor temperature
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
|
* sensors.use_sensors: whether to use the 'sensors' command.
|
||||||
|
If set to 'false', the sysfs-interface at '/sys/class/thermal' is used.
|
||||||
|
If not set, 'sensors' will be used if available.
|
||||||
* sensors.path: path to temperature file (default /sys/class/thermal/thermal_zone0/temp).
|
* sensors.path: path to temperature file (default /sys/class/thermal/thermal_zone0/temp).
|
||||||
* sensors.json: if set to 'true', interpret sensors.path as JSON 'path' in the output
|
* sensors.json: if set to 'true', interpret sensors.path as JSON 'path' in the output
|
||||||
of 'sensors -j' (i.e. <key1>/<key2>/.../<value>), for example, path could
|
of 'sensors -j' (i.e. <key1>/<key2>/.../<value>), for example, path could
|
||||||
|
@ -1147,12 +1261,12 @@ Shows a widget per user-defined shortcut and allows to define the behaviour
|
||||||
when clicking on it.
|
when clicking on it.
|
||||||
|
|
||||||
For more than one shortcut, the commands and labels are strings separated by
|
For more than one shortcut, the commands and labels are strings separated by
|
||||||
a demiliter (; semicolon by default).
|
a delimiter (; semicolon by default).
|
||||||
|
|
||||||
For example in order to create two shortcuts labeled A and B with commands
|
For example in order to create two shortcuts labeled A and B with commands
|
||||||
cmdA and cmdB you could do:
|
cmdA and cmdB you could do:
|
||||||
|
|
||||||
./bumblebee-status -m shortcut -p shortcut.cmd='ls;ps' shortcut.label='A;B'
|
./bumblebee-status -m shortcut -p shortcut.cmd='firefox https://www.google.com;google-chrome https://google.com' shortcut.label='Google (Firefox);Google (Chrome)'
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
* shortcut.cmds : List of commands to execute
|
* shortcut.cmds : List of commands to execute
|
||||||
|
@ -1174,7 +1288,7 @@ Requires the following executables:
|
||||||
* smartctl
|
* smartctl
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
* smartstatus.display: how to display (defaults to 'combined', other choices: 'seperate' or 'singles')
|
* smartstatus.display: how to display (defaults to 'combined', other choices: 'combined_singles', 'seperate' or 'singles')
|
||||||
* smartstatus.drives: in the case of singles which drives to display, separated comma list value, multiple accepted (defaults to 'sda', example:'sda,sdc')
|
* smartstatus.drives: in the case of singles which drives to display, separated comma list value, multiple accepted (defaults to 'sda', example:'sda,sdc')
|
||||||
* smartstatus.show_names: boolean in the form of "True" or "False" to show the name of the drives in the form of sda, sbd, combined or none at all.
|
* smartstatus.show_names: boolean in the form of "True" or "False" to show the name of the drives in the form of sda, sbd, combined or none at all.
|
||||||
|
|
||||||
|
@ -1187,7 +1301,6 @@ an example.
|
||||||
|
|
||||||
Requires the following libraries:
|
Requires the following libraries:
|
||||||
* requests
|
* requests
|
||||||
* regex
|
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
* spaceapi.url: String representation of the api endpoint
|
* spaceapi.url: String representation of the api endpoint
|
||||||
|
@ -1218,6 +1331,10 @@ Parameters:
|
||||||
Available values are: {album}, {title}, {artist}, {trackNumber}
|
Available values are: {album}, {title}, {artist}, {trackNumber}
|
||||||
* spotify.layout: Comma-separated list to change order of widgets (defaults to song, previous, pause, next)
|
* spotify.layout: Comma-separated list to change order of widgets (defaults to song, previous, pause, next)
|
||||||
Widget names are: spotify.song, spotify.prev, spotify.pause, spotify.next
|
Widget names are: spotify.song, spotify.prev, spotify.pause, spotify.next
|
||||||
|
* spotify.concise_controls: When enabled, allows spotify to be controlled from just the spotify.song widget.
|
||||||
|
Concise controls are: Left Click: Toggle Pause; Wheel Up: Next; Wheel Down; Previous.
|
||||||
|
* spotify.bus_name: String (defaults to `spotify`)
|
||||||
|
Available values: spotify, spotifyd
|
||||||
|
|
||||||
contributed by `yvesh <https://github.com/yvesh>`_ - many thanks!
|
contributed by `yvesh <https://github.com/yvesh>`_ - many thanks!
|
||||||
|
|
||||||
|
@ -1232,9 +1349,6 @@ stock
|
||||||
|
|
||||||
Display a stock quote from finance.yahoo.com
|
Display a stock quote from finance.yahoo.com
|
||||||
|
|
||||||
Requires the following python packages:
|
|
||||||
* requests
|
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
* stock.symbols : Comma-separated list of symbols to fetch
|
* stock.symbols : Comma-separated list of symbols to fetch
|
||||||
* stock.change : Should we fetch change in stock value (defaults to True)
|
* stock.change : Should we fetch change in stock value (defaults to True)
|
||||||
|
@ -1255,8 +1369,8 @@ Requires the following python packages:
|
||||||
* python-dateutil
|
* python-dateutil
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
* cpu.lat : Latitude of your location
|
* sun.lat : Latitude of your location
|
||||||
* cpu.lon : Longitude of your location
|
* sun.lon : Longitude of your location
|
||||||
|
|
||||||
(if none of those are set, location is determined automatically via location APIs)
|
(if none of those are set, location is determined automatically via location APIs)
|
||||||
|
|
||||||
|
@ -1285,6 +1399,9 @@ Parameters:
|
||||||
* system.suspend: specify a command for suspending (defaults to 'i3exit suspend')
|
* system.suspend: specify a command for suspending (defaults to 'i3exit suspend')
|
||||||
* system.hibernate: specify a command for hibernating (defaults to 'i3exit hibernate')
|
* system.hibernate: specify a command for hibernating (defaults to 'i3exit hibernate')
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
tkinter (python3-tk package on debian based systems either you can install it as python package)
|
||||||
|
|
||||||
contributed by `bbernhard <https://github.com/bbernhard>`_ - many thanks!
|
contributed by `bbernhard <https://github.com/bbernhard>`_ - many thanks!
|
||||||
|
|
||||||
taskwarrior
|
taskwarrior
|
||||||
|
@ -1303,6 +1420,24 @@ contributed by `chdorb <https://github.com/chdorb>`_ - many thanks!
|
||||||
|
|
||||||
.. image:: ../screenshots/taskwarrior.png
|
.. image:: ../screenshots/taskwarrior.png
|
||||||
|
|
||||||
|
thunderbird
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
Displays the unread emails count for one or more Thunderbird inboxes
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
* thunderbird.home: Absolute path of your .thunderbird directory (e.g.: /home/pi/.thunderbird)
|
||||||
|
* thunderbird.inboxes: Comma separated values for all MSF inboxes and their parent directory (account) (e.g.: imap.gmail.com/INBOX.msf,outlook.office365.com/Work.msf)
|
||||||
|
|
||||||
|
Tips:
|
||||||
|
* You can run the following command in order to list all your Thunderbird inboxes
|
||||||
|
|
||||||
|
find ~/.thunderbird -name '*.msf' | awk -F '/' '{print $(NF-1)"/"$(NF)}'
|
||||||
|
|
||||||
|
contributed by `cristianmiranda <https://github.com/cristianmiranda>`_ - many thanks!
|
||||||
|
|
||||||
|
.. image:: ../screenshots/thunderbird.png
|
||||||
|
|
||||||
timetz
|
timetz
|
||||||
~~~~~~
|
~~~~~~
|
||||||
|
|
||||||
|
@ -1343,6 +1478,15 @@ contributed by `codingo <https://github.com/codingo>`_ - many thanks!
|
||||||
|
|
||||||
.. image:: ../screenshots/todo.png
|
.. image:: ../screenshots/todo.png
|
||||||
|
|
||||||
|
todo_org
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
Displays the number of todo items from an org-mode file
|
||||||
|
Parameters:
|
||||||
|
* todo_org.file: File to read TODOs from (defaults to ~/org/todo.org)
|
||||||
|
* todo_org.remaining: False by default. When true, will output the number of remaining todos instead of the number completed (i.e. 1/4 means 1 of 4 todos remaining, rather than 1 of 4 todos completed)
|
||||||
|
Based on the todo module by `codingo <https://github.com/codingo>`
|
||||||
|
|
||||||
traffic
|
traffic
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|
||||||
|
|
1
docs/requirements.txt
Normal file
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
|
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
|
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.6
|
||||||
Programming Language :: Python :: 3.7
|
Programming Language :: Python :: 3.7
|
||||||
Programming Language :: Python :: 3.8
|
Programming Language :: Python :: 3.8
|
||||||
|
Programming Language :: Python :: 3.9
|
||||||
Topic :: Software Development :: Libraries
|
Topic :: Software Development :: Libraries
|
||||||
Topic :: Software Development :: Internationalization
|
Topic :: Software Development :: Internationalization
|
||||||
Topic :: Utilities
|
Topic :: Utilities
|
||||||
|
@ -30,8 +31,8 @@ keywords = bumblebee-status
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
include_package_data = True
|
include_package_data = True
|
||||||
allow-all-external = yes
|
allow_all_external = yes
|
||||||
trusted-host =
|
trusted_host =
|
||||||
gitlab.*
|
gitlab.*
|
||||||
bitbucket.org
|
bitbucket.org
|
||||||
github.com
|
github.com
|
||||||
|
|
5
setup.py
5
setup.py
|
@ -1,7 +1,7 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
"""Setup file for bumbleestatus bar to allow pip install of full package"""
|
"""Setup file for bumbleestatus bar to allow pip install of full package"""
|
||||||
# -*- coding: utf8 - *-
|
# -*- coding: utf8 - *-
|
||||||
from setuptools import setup
|
from setuptools import setup, find_packages
|
||||||
import versioneer
|
import versioneer
|
||||||
|
|
||||||
with open("requirements/base.txt") as f:
|
with open("requirements/base.txt") as f:
|
||||||
|
@ -20,11 +20,9 @@ EXTRAS_REQUIREMENTS_MAP = {
|
||||||
"cpu2": read_module("cpu2"),
|
"cpu2": read_module("cpu2"),
|
||||||
"currency": read_module("currency"),
|
"currency": read_module("currency"),
|
||||||
"docker_ps": read_module("docker_ps"),
|
"docker_ps": read_module("docker_ps"),
|
||||||
"dunst": read_module("dunst"),
|
|
||||||
"getcrypto": read_module("getcrypto"),
|
"getcrypto": read_module("getcrypto"),
|
||||||
"git": read_module("git"),
|
"git": read_module("git"),
|
||||||
"github": read_module("github"),
|
"github": read_module("github"),
|
||||||
"hddtemp": read_module("hddtemp"),
|
|
||||||
"layout-xkb": read_module("layout_xkb"),
|
"layout-xkb": read_module("layout_xkb"),
|
||||||
"memory": read_module("memory"),
|
"memory": read_module("memory"),
|
||||||
"network_traffic": read_module("network_traffic"),
|
"network_traffic": read_module("network_traffic"),
|
||||||
|
@ -59,4 +57,5 @@ setup(
|
||||||
("share/bumblebee-status/themes/icons", glob.glob("themes/icons/*.json")),
|
("share/bumblebee-status/themes/icons", glob.glob("themes/icons/*.json")),
|
||||||
("share/bumblebee-status/utility", glob.glob("bin/*")),
|
("share/bumblebee-status/utility", glob.glob("bin/*")),
|
||||||
],
|
],
|
||||||
|
packages=find_packages(exclude=["tests", "tests.*"])
|
||||||
)
|
)
|
||||||
|
|
|
@ -55,7 +55,8 @@ def test_importerror(mocker):
|
||||||
module = core.module.load(module_name="test", config=config)
|
module = core.module.load(module_name="test", config=config)
|
||||||
|
|
||||||
assert module.__class__.__name__ == "Error"
|
assert module.__class__.__name__ == "Error"
|
||||||
assert module.widget().full_text() == "test: some-error"
|
assert module.widget().full_text() == "test: some-error" or \
|
||||||
|
module.widget().full_text() == "test: unable to load module"
|
||||||
|
|
||||||
|
|
||||||
def test_loadvalid_module():
|
def test_loadvalid_module():
|
||||||
|
|
|
@ -26,6 +26,7 @@ def module_a(mocker):
|
||||||
widget = mocker.MagicMock()
|
widget = mocker.MagicMock()
|
||||||
widget.full_text.return_value = "test"
|
widget.full_text.return_value = "test"
|
||||||
widget.id = "a"
|
widget.id = "a"
|
||||||
|
widget.hidden = False
|
||||||
return SampleModule(config=core.config.Config([]), widgets=[widget, widget, widget])
|
return SampleModule(config=core.config.Config([]), widgets=[widget, widget, widget])
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
|
@ -1,5 +1,159 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
import util.cli
|
||||||
|
import core.config
|
||||||
|
import modules.contrib.amixer
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def module_mock(request):
|
||||||
|
def _module_mock(config = []):
|
||||||
|
return modules.contrib.amixer.Module(
|
||||||
|
config=core.config.Config(config),
|
||||||
|
theme=None
|
||||||
|
)
|
||||||
|
|
||||||
|
yield _module_mock
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def amixer_mock():
|
||||||
|
def _mock(device='Master', volume='10%', state='on'):
|
||||||
|
return """
|
||||||
|
Simple mixer control '{device}',0
|
||||||
|
Capabilities: pvolume pswitch pswitch-joined
|
||||||
|
Playback channels: Front Left - Front Right
|
||||||
|
Limits: Playback 0 - 65536
|
||||||
|
Mono:
|
||||||
|
Front Left: Playback 55705 [{volume}%] [{state}]
|
||||||
|
Front Right: Playback 55705 [{volume}%] [{state}]
|
||||||
|
""".format(
|
||||||
|
device=device,
|
||||||
|
volume=volume,
|
||||||
|
state=state
|
||||||
|
)
|
||||||
|
|
||||||
|
return _mock
|
||||||
|
|
||||||
def test_load_module():
|
def test_load_module():
|
||||||
__import__("modules.contrib.amixer")
|
__import__("modules.contrib.amixer")
|
||||||
|
|
||||||
|
def test_initial_full_text(module_mock, amixer_mock, mocker):
|
||||||
|
module = module_mock()
|
||||||
|
assert module.widget().full_text() == 'n/a'
|
||||||
|
|
||||||
|
def test_input_registration(mocker):
|
||||||
|
input_register = mocker.patch('core.input.register')
|
||||||
|
|
||||||
|
module = modules.contrib.amixer.Module(
|
||||||
|
config=core.config.Config([]),
|
||||||
|
theme=None
|
||||||
|
)
|
||||||
|
|
||||||
|
input_register.assert_any_call(
|
||||||
|
module,
|
||||||
|
button=core.input.WHEEL_DOWN,
|
||||||
|
cmd=module.decrease_volume
|
||||||
|
)
|
||||||
|
|
||||||
|
input_register.assert_any_call(
|
||||||
|
module,
|
||||||
|
button=core.input.WHEEL_UP,
|
||||||
|
cmd=module.increase_volume
|
||||||
|
)
|
||||||
|
|
||||||
|
input_register.assert_any_call(
|
||||||
|
module,
|
||||||
|
button=core.input.LEFT_MOUSE,
|
||||||
|
cmd=module.toggle
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_volume_update(module_mock, amixer_mock, mocker):
|
||||||
|
mocker.patch(
|
||||||
|
'util.cli.execute',
|
||||||
|
return_value=amixer_mock(volume='25%', state='on')
|
||||||
|
)
|
||||||
|
|
||||||
|
module = module_mock()
|
||||||
|
widget = module.widget()
|
||||||
|
|
||||||
|
module.update()
|
||||||
|
assert widget.full_text() == '25%'
|
||||||
|
assert module.state(widget) == ['unmuted']
|
||||||
|
|
||||||
|
def test_muted_update(module_mock, amixer_mock, mocker):
|
||||||
|
mocker.patch(
|
||||||
|
'util.cli.execute',
|
||||||
|
return_value=amixer_mock(volume='50%', state='off')
|
||||||
|
)
|
||||||
|
|
||||||
|
module = module_mock()
|
||||||
|
widget = module.widget()
|
||||||
|
|
||||||
|
module.update()
|
||||||
|
assert widget.full_text() == '50%'
|
||||||
|
assert module.state(widget) == ['warning', 'muted']
|
||||||
|
|
||||||
|
def test_exception_update(module_mock, mocker):
|
||||||
|
mocker.patch(
|
||||||
|
'util.cli.execute',
|
||||||
|
side_effect=Exception
|
||||||
|
)
|
||||||
|
|
||||||
|
module = module_mock()
|
||||||
|
widget = module.widget()
|
||||||
|
|
||||||
|
module.update()
|
||||||
|
assert widget.full_text() == 'n/a'
|
||||||
|
|
||||||
|
def test_unavailable_amixer(module_mock, mocker):
|
||||||
|
mocker.patch('util.cli.execute', return_value='Invalid')
|
||||||
|
|
||||||
|
module = module_mock()
|
||||||
|
widget = module.widget()
|
||||||
|
|
||||||
|
module.update()
|
||||||
|
assert widget.full_text() == '0%'
|
||||||
|
|
||||||
|
def test_toggle(module_mock, mocker):
|
||||||
|
command = mocker.patch('util.cli.execute')
|
||||||
|
module = module_mock()
|
||||||
|
module.toggle(False)
|
||||||
|
command.assert_called_once_with('amixer -q set Master,0 toggle')
|
||||||
|
|
||||||
|
def test_default_volume(module_mock, mocker):
|
||||||
|
module = module_mock()
|
||||||
|
|
||||||
|
command = mocker.patch('util.cli.execute')
|
||||||
|
module.increase_volume(False)
|
||||||
|
command.assert_called_once_with('amixer -q set Master,0 4%+')
|
||||||
|
|
||||||
|
command = mocker.patch('util.cli.execute')
|
||||||
|
module.decrease_volume(False)
|
||||||
|
command.assert_called_once_with('amixer -q set Master,0 4%-')
|
||||||
|
|
||||||
|
def test_custom_volume(module_mock, mocker):
|
||||||
|
module = module_mock(['-p', 'amixer.percent_change=25'])
|
||||||
|
|
||||||
|
command = mocker.patch('util.cli.execute')
|
||||||
|
module.increase_volume(False)
|
||||||
|
command.assert_called_once_with('amixer -q set Master,0 25%+')
|
||||||
|
|
||||||
|
command = mocker.patch('util.cli.execute')
|
||||||
|
module.decrease_volume(False)
|
||||||
|
command.assert_called_once_with('amixer -q set Master,0 25%-')
|
||||||
|
|
||||||
|
def test_custom_device(module_mock, mocker):
|
||||||
|
mocker.patch('util.cli.execute')
|
||||||
|
module = module_mock(['-p', 'amixer.device=CustomMaster'])
|
||||||
|
|
||||||
|
command = mocker.patch('util.cli.execute')
|
||||||
|
module.toggle(False)
|
||||||
|
command.assert_called_once_with('amixer -q set CustomMaster toggle')
|
||||||
|
|
||||||
|
command = mocker.patch('util.cli.execute')
|
||||||
|
module.increase_volume(False)
|
||||||
|
command.assert_called_once_with('amixer -q set CustomMaster 4%+')
|
||||||
|
|
||||||
|
command = mocker.patch('util.cli.execute')
|
||||||
|
module.decrease_volume(False)
|
||||||
|
command.assert_called_once_with('amixer -q set CustomMaster 4%-')
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,76 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
import util.cli
|
||||||
|
import core.config
|
||||||
|
import modules.contrib.arch_update
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def module():
|
||||||
|
module = modules.contrib.arch_update.Module(
|
||||||
|
config=core.config.Config([]),
|
||||||
|
theme=None
|
||||||
|
)
|
||||||
|
|
||||||
|
yield module
|
||||||
|
|
||||||
def test_load_module():
|
def test_load_module():
|
||||||
__import__("modules.contrib.arch-update")
|
__import__("modules.contrib.arch-update")
|
||||||
|
|
||||||
|
def test_load_symbolic_link_module():
|
||||||
|
__import__("modules.contrib.arch_update")
|
||||||
|
|
||||||
|
def test_with_one_package(module, mocker):
|
||||||
|
command = mocker.patch(
|
||||||
|
'util.cli.execute',
|
||||||
|
return_value=(0, 'bumblebee 1.0.0')
|
||||||
|
)
|
||||||
|
|
||||||
|
module.update()
|
||||||
|
|
||||||
|
command.assert_called_with(
|
||||||
|
'checkupdates',
|
||||||
|
ignore_errors=True,
|
||||||
|
return_exitcode=True
|
||||||
|
)
|
||||||
|
|
||||||
|
widget = module.widget()
|
||||||
|
assert widget.full_text() == 'Update Arch: 1'
|
||||||
|
assert module.state(widget) == None
|
||||||
|
assert module.hidden() == False
|
||||||
|
|
||||||
|
def test_with_two_packages(module, mocker):
|
||||||
|
command = mocker.patch(
|
||||||
|
'util.cli.execute',
|
||||||
|
return_value=(0, 'bumblebee 1.0.0\ni3wm 3.5.7')
|
||||||
|
)
|
||||||
|
|
||||||
|
module.update()
|
||||||
|
|
||||||
|
widget = module.widget()
|
||||||
|
assert widget.full_text() == 'Update Arch: 2'
|
||||||
|
assert module.state(widget) == 'warning'
|
||||||
|
assert module.hidden() == False
|
||||||
|
|
||||||
|
def test_with_no_packages(module, mocker):
|
||||||
|
mocker.patch('util.cli.execute', return_value=(2, ''))
|
||||||
|
|
||||||
|
module.update()
|
||||||
|
|
||||||
|
widget = module.widget()
|
||||||
|
assert widget.full_text() == 'Update Arch: 0'
|
||||||
|
assert module.state(widget) == None
|
||||||
|
assert module.hidden() == True
|
||||||
|
|
||||||
|
def test_with_unknown_code(module, mocker):
|
||||||
|
mocker.patch('util.cli.execute', return_value=(99, 'error'))
|
||||||
|
logger = mocker.patch('logging.error')
|
||||||
|
|
||||||
|
module.update()
|
||||||
|
|
||||||
|
logger.assert_called_with('checkupdates exited with {}: {}'.format(99, 'error'))
|
||||||
|
|
||||||
|
widget = module.widget()
|
||||||
|
assert widget.full_text() == 'Update Arch: 0'
|
||||||
|
assert module.state(widget) == 'warning'
|
||||||
|
assert module.hidden() == False
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue